diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..bd98b4f --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "shadcn": { + "command": "npx", + "args": [ + "shadcn@latest", + "mcp" + ] + } + } +} diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..2f37ac2 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,288 @@ +# Pulse — Architecture + +Pulse is a single Next.js 16 app (`output: 'standalone'`) that backs Wulf +Consulting's PSA workflows. It pulls data from Autotask, Datto RMM, IT Glue, MS +Graph, Veeam, and ~10 other systems into Postgres, runs background workers for +sync / AI analysis / RMM execution, and serves dashboards + admin tooling on +port **3100**. + +This file is the load-bearing reference for *how the system is wired*. Per- +feature deep dives live in `docs/`. UI/visual conventions live in `DESIGN.md`. + +## 1. Runtime topology + +One Node process, one Postgres, one Redis. Background work runs **in-process** +inside the Next server — there is no external job queue. + +``` +┌─────────────────────────────────────────────────────────┐ +│ Next.js 16 (port 3100, output: 'standalone') │ +│ │ +│ HTTP routes ──► API handlers ──► Postgres / Redis │ +│ │ +│ Side-effect imports auto-start three workers: │ +│ • SyncScheduler (node-cron) │ +│ • AnalyzerWorker (poll analyzer_jobs every 2s) │ +│ • RmmOvershellWorker (poll rmm_executions every 5s) │ +└─────────────────────────────────────────────────────────┘ + │ │ │ + ▼ ▼ ▼ + Postgres 16 Redis 7 External APIs + (state) (cache only) (Autotask, RMM, …) +``` + +**Workers are side-effect imports.** Touching one of these modules from a +server-side import path starts the loop: + +| Worker | File | Trigger | +|---|---|---| +| Sync scheduler | `lib/services/sync-scheduler.ts` | Self-init at module bottom | +| Analyzer | `lib/services/analyzer/worker.ts` | Auto-starts when `NODE_ENV=production` or `ANALYZER_WORKER_AUTOSTART=1` | +| RMM Overshell | `lib/services/rmm/worker.ts` | Same gate as analyzer | + +Routes that need a worker running deliberately import its module — e.g. +`app/api/analyzer/tickets/[ticketNumber]/analyze/route.ts` imports +`lib/services/analyzer/worker.ts` purely so the loop starts on first request. +**Don't eager-import these from hot paths or shared utilities.** + +There is **no out-of-process queue**. Scaling out to multiple Next instances +means each instance runs duplicate pollers. The analyzer worker uses +`SELECT … FOR UPDATE SKIP LOCKED` so jobs run exactly once across instances, +but the sync scheduler does not — run it on a single dedicated instance, or +gate it behind an env flag on replicas. + +## 2. Data flow + +### Autotask (primary system of record) +- **Webhook** — `POST /api/webhooks/autotask`, public per `middleware.ts`, + HMAC-verified inside the handler. Returns 200 even on processing errors so + Autotask doesn't deactivate the subscription. Async handlers may enqueue + analyzer jobs. +- **Periodic sync** — `lib/services/entity-sync.ts` runs per scheduled task. + Incremental via `lastTrackedModificationDateTime` when supported; full upsert + otherwise. Targets `tickets`, `companies`, `contacts`, `resources`, `tasks`, + `time_entries`, etc. + +### Datto RMM +- **Sync** — devices, sites, alerts → `datto_rmm_*` tables. +- **Overshell executor** — `lib/services/rmm/executor.ts` validates a + registered script ID, resolves the target device, enforces a per-user rate + limit (50 / 24h), inserts a `pending` row in `rmm_executions`, and calls + `client.runQuickJob()`. The worker polls Datto for the result and parses + output via the script's `parseOutput()` method. Scripts are **code- + registered** (`lib/services/rmm/scripts/`) — adding one is a TS change, not a + DB change. +- **LogLift** — `POST /api/rmm/loglift/upload` (public, `x-openclaw-key` + header). Receives a B2 object key, downloads + decompresses the gzipped JSON + (capped at 100 MB), stores a slim summary in `loglift_uploads` and the full + payload in B2 (`lib/services/b2/client.ts`). Resolves the device → Autotask + company → IT Glue configuration; if a unique IT Glue match is found, fires + an asset-first audit automatically. + +### IT Glue +- **Sync** — `lib/services/itglue-sync-service.ts` pulls org types, configs, + contacts, locations, flexible assets, etc. → `itg_*` tables. Note: flexible + assets must be listed per type (API 422 otherwise — see commit `a0a6e7f`). +- **Search (analyzer)** — `lib/services/analyzer/itglue-search.ts` returns + redacted documents only. Credentials/PII pass through `redact()` before any + LLM sees them. Callers must not bypass redaction; the raw client is for non- + LLM use. +- **Audit + write-back** — `lib/services/analyzer/asset-audit/` runs LLM-driven + audits against IT Glue configurations or flexible assets, writes results to + `itglue_audit_logs`, and links tickets via `itglue_ticket_xrefs`. Reverts go + through `…/revert/[writeId]`. + +### MS Graph (Engagement) +- App-only auth, **specific tenant ID** (not `common`). Reports API returns + CSV; parsed inline. Joins to Autotask hours via `resources.email = + graph_users.email`. See `lib/services/engagement-sync-service.ts`. + +### Other integrations +Each has a factory + `isConfigured()` helper in `lib/services/`. All +credentials come from env; clients throw if missing. + +| System | Role | Files | +|---|---|---| +| Veeam VSPC | Backup status, RPO, ticket analysis | `veeam-*-service.ts` | +| Auvik | Network monitoring; tenant mappings | `auvik-client.ts` | +| Addigy | Apple endpoints; org mappings | `addigy-factory.ts` | +| Mimecast | Mail security | `mimecast-sync-service.ts` | +| SentinelOne | EDR | `sentinelone-sync-service.ts` | +| Duo | MFA | `duo-sync-service.ts` | +| Zoom | Meetings | `zoom-sync-service.ts` | +| QuickBooks | Billing reconciliation | `qbo-sync-service.ts` | +| Zabbix | WAN monitoring; webhook at `/api/zabbix/webhook` | — | +| Salesbldr | Sales pipeline | — | + +## 3. Analyzer pipeline + +`lib/services/analyzer/pipeline.ts` orchestrates seven stages. Provider is +chosen per request (`anthropic` default, `openrouter` opt-in); models are +mapped per stage in `lib/services/llm/models.ts`. + +| Stage | Model (Anthropic) | Purpose | +|---|---|---| +| 0 — Preprocess | — | Filter workflow noise, tag entities, compute `content_hash` (idempotency key) | +| 1 — Triage | Haiku | Categorize, extract entities, initial priority | +| 2 — IT Glue retrieval | — | Redacted doc lookup, skipped if IT Glue not configured | +| 3 — Deep analysis | Sonnet | Summary, gaps, what-was-done, what-should-have-been-done | +| 4 — Deep reasoning | Opus (optional) | Apply corrections, propose IT Glue updates, re-rank | +| 5 — Persist | — | Write `analyzer_analyses` row + per-stage execution rows | +| 6 — Fingerprint | Haiku | Structured fingerprint for cross-ticket aggregation | + +**Idempotency.** If `content_hash` already exists for the ticket and +`force=false`, the pipeline returns the existing analysis. The hash is +provider-scoped — Claude and DeepSeek analyses of the same ticket are separate +rows. + +**Cost ceiling.** Stage 4 is skipped if estimated total cost exceeds **$2.00**; +the analysis is flagged for human review. All LLM/RMM activity is logged to +`analyzer_cost_audit`. + +**Link-aware bundles.** `lib/services/analyzer/link-discovery.ts` resolves +related tickets two ways: (1) explicit — regex T-numbers in descriptions/notes, +"RELATED TICKETS:" blocks, the `problem_ticket_id` column; (2) suggested +(opt-in) — Haiku ranks recent same-company tickets by semantic similarity. When +a bundle is analyzed, members get `pending_analyses` rows; once all complete, +an aggregate report fires. + +**Aggregate reports.** `stages/aggregate-reduce.ts` pairs SQL distributions +(category, client, resolution path, root cause) with a Sonnet pass that +identifies documentation gaps, process gaps, client patterns, recurrence +clusters. Persisted to `analyzer_aggregate_reports`. + +**Asset audit (Phase 4).** `lib/services/analyzer/asset-audit/runner.ts` runs +post-analysis. Two modes: all-time evidence across every analysis linked to +the asset, or ticket-first (Phase 4.1) narrowed to a single analysis ID. + +## 4. Background jobs + +| Worker | Cadence | Scope | Concurrency model | +|---|---|---|---| +| `AnalyzerWorker` | 2s poll | Claims `analyzer_jobs.status='queued'` | `FOR UPDATE SKIP LOCKED`, exactly-once across instances | +| `RmmOvershellWorker` | 5s poll | Polls in-flight `rmm_executions`; advances state by querying Datto | Single in-process loop | +| `SyncScheduler` | node-cron | Per-schedule rows in DB (Autotask, IT Glue, Veeam, Engagement, Zoom, Duo, …) | **Not** safe for multi-instance — overlaps possible | +| `integration-health-alerts` | Cron-fired from sync scheduler | Detects stale syncs, publishes alerts | Single in-process | + +Stale in-flight analyzer jobs are reset on worker boot (commit `378e68a`) so a +crashed pod doesn't leave jobs orphaned. + +## 5. Auth & permissions + +**Better Auth 1.4** with magic link + TOTP 2FA + Microsoft OAuth. Sessions +live in Postgres (no Redis session store). Account-linking is enabled for +Microsoft so admin-invited users join their MS account in one click. + +**Roles.** `user`, `admin`, `super-admin`. Default admin bootstrapped from +`DEFAULT_ADMIN_EMAIL` via `lib/bootstrap.ts`. + +**Resources** (`lib/permissions.ts`) — `tickets`, `configItems`, `admin`, +`users`, `roles`, `auditLog`, `settings`, `itglue`, `rmm`. The `itglue` and +`rmm` resources were added with the Overshell + IT Glue write-back work; user +role gets read-only on both. + +**API auth pattern.** Every route handler calls one of: +```ts +const { session, error } = await requireAuth(); +const { session, error } = await requireAdmin(); +const { session, error } = await requireSuperAdmin(); +const { session, error } = await requirePermission('itglue', 'write'); +if (error) return error; +``` +`middleware.ts` only checks for a session cookie — role/permission checks live +in the route handler. + +**Public routes** (hardcoded in `middleware.ts`): `/api/auth/*`, +`/api/webhooks/*`, `/api/sync/*`, `/api/health`, `/api/zabbix/webhook`, +`/api/rmm/loglift`, `/api/mobile/*`, `/api/openclaw/*`, `/legal`, +`/api/kiosk`, `/api/qbo/*`. **Add to that list whenever you introduce a new +public endpoint.** + +## 6. Database + +89 numbered migrations (`migrations/NNN_*.sql`), applied in **alphabetical** +order on Postgres init only. Existing volumes do not re-run them — for +schema changes against an existing DB, use `scripts/apply-migrations` (verify +behavior first; varies by age of script). + +Topical groupings: + +| Range | Topic | +|---|---| +| 001–014 | Core schema, auth tables, admin settings | +| 015–026 | Ticket fields, queues, RMM site mappings, integration health | +| 027–032 | Datto RMM, Veeam agents/alarms, priorities, ticket categories, RMM webhooks | +| 037–044 | IT Glue (large), Veeam RPO, contract services, engagement | +| 045–055 | Zoom, Teams, morning summary, ping suppression, ticket digest, Zabbix WAN, QBO, Mimecast | +| 056–068 | UDFs, Autotask tags, Duo, project phases, recurring revenue, Veeam ticket analysis | +| 069–074 | Analyzer (jobs, analyses, stage executions, aggregate reports, cost audit, link-aware bundles, provider) | +| 075–076 | IT Glue audit + ticket xrefs | +| 077–078 | RMM Overshell, LogLift uploads | +| 079–080 | Endpoint data model, device-xref `company_id` | + +**Watch out for:** +- Duplicate numbers exist (002, 004, 009). Apply order is filesystem-sort + alphabetical, not numeric. Don't introduce more. +- Conventions: `IF NOT EXISTS` for tables/indexes, `ON CONFLICT DO NOTHING` + for seed data, audit columns `created_at` / `updated_at` / `synced_at` / + `is_deleted` / `deleted_at`. +- `priorities` has no `is_deleted` column — caught the hard way (`9acf48e`). +- Columns are **`snake_case`**; API responses are **`camelCase`**. Handlers + transform manually. No ORM. + +DB access is the singleton at `lib/services/postgres-client.ts` — +`postgresClient.query()`, `.transaction()`, `.upsert()`, `.bulkUpsert()`. + +## 7. Deployment + +**Docker Compose** at the repo root. +- `postgres` — Postgres 16, port 5432. Migrations volume mounted at + `/docker-entrypoint-initdb.d/`. Init runs once per volume. +- `redis` — Redis 7, port 6380 (host) / 6379 (container). Cache only. +- `app` — built from `Dockerfile` (turbopack, `output: 'standalone'`); runs + `node server.js`. Port 3100. `.env.local` mounted read-only. Traefik labels + for `pulse.wulfconsulting.cloud` (HTTPS via Cloudflare cert). + +`npm run build` uses turbopack (Next 16 default). `npm run dev` for local; +`npx tsc --noEmit --pretty` for type check; `npm test` (vitest) for the +analyzer / RMM / B2 / link-discovery unit tests. **No CI**; type-check is the +only safety net for code that doesn't have unit tests. + +## 8. Invariants & gotchas + +1. **Worker side-effect imports.** Importing `sync-scheduler.ts`, + `analyzer/worker.ts`, or `rmm/worker.ts` from a hot path starts the loop. +2. **No external queue.** Multiple instances duplicate pollers. Analyzer is + safe via row locking; sync scheduler is not — pin to one instance. +3. **IT Glue redaction is mandatory** for any LLM-bound query. Use + `itglue-search.ts`, never the raw client. +4. **Provider-scoped idempotency.** `force=false` only short-circuits if the + same provider produced the existing analysis. +5. **Cost ceiling at $2.00** before Stage 4. Above that, Opus is skipped and + the analysis is flagged. +6. **Webhook handlers return 200 on failure** (Autotask) to avoid + deactivation. Errors are logged, not surfaced. +7. **Postgres init runs migrations once.** Existing volumes won't re-run them. +8. **Duplicate migration numbers.** Apply order is alphabetic. +9. **`.env` is committed.** Treat the values as potentially real production + secrets; don't log or echo them. +10. **RMM script registry is in code.** `lib/services/rmm/scripts/` — + unregistered scripts can't execute. +11. **LogLift zip-bomb guard** caps inflated payloads at 100 MB. +12. **Stale analyzer jobs reset on worker boot.** Don't rely on `in_flight` + state surviving restarts. + +## 9. Where to look + +| Concern | Start here | +|---|---| +| HTTP routes | `app/api/**/route.ts` | +| Pages | `app/**/page.tsx` | +| Worker boot | `lib/services/{sync-scheduler,analyzer/worker,rmm/worker}.ts` | +| Postgres access | `lib/services/postgres-client.ts` | +| Auth wiring | `lib/auth.ts`, `lib/auth-utils.ts`, `lib/permissions.ts`, `middleware.ts` | +| Analyzer pipeline | `lib/services/analyzer/pipeline.ts` + `stages/` | +| LLM dispatch | `lib/services/llm/{call,models,pricing}.ts` | +| RMM executor | `lib/services/rmm/{executor,worker,target-resolver}.ts` | +| IT Glue write-back | `lib/services/analyzer/asset-audit/` | +| Per-feature notes | `docs/` (one file per system) | diff --git a/CLAUDE.md b/CLAUDE.md index ca774e2..7214769 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,13 @@ data into Postgres and adds dashboards, workflows, and analytics around it. Single Next.js 16 app — not a monorepo. `README.md` covers the human-facing overview. **Trust this file** for the details -that matter to coding decisions. +that matter to coding decisions. For deeper context: + +- **`ARCHITECTURE.md`** — runtime topology, data flow, workers, analyzer pipeline, + invariants. Read before touching workers, sync, or the analyzer. +- **`DESIGN.md`** — design tokens, navigation IA, component vocabulary, layout + rules, and the working backlog for nav/visual cleanup. Read before touching + pages or shared UI components. ## Stack - Next.js 16 + React 19 (App Router, `reactCompiler: true`, `output: 'standalone'`) @@ -73,7 +79,9 @@ Examples: `getAutotaskClient()`, `getMsgraphClient()`, `getDattoRmmClient()`, | Datto RMM | `DATTO_RMM_*` | | Veeam VSPC | `VEEAM_VSPC_*` | | Auvik / Addigy / IT Glue / Mimecast / S1 / Duo / Zoom / QBO / Zabbix / Salesbldr | `_*` | -| Anthropic | `ANTHROPIC_API_KEY` (used in `ai-triage-service.ts`, `llm-analyzer.ts`) | +| Anthropic | `ANTHROPIC_API_KEY` (analyzer pipeline + `ai-triage-service.ts`) | +| OpenRouter | `OPENROUTER_API_KEY` (alternate analyzer provider, opt-in per request) | +| Backblaze B2 | `B2_*` (LogLift evidence storage) | | Postgres / Redis | `POSTGRES_*` or `DATABASE_URL`, `REDIS_URL` | ## Sync & scheduling @@ -82,8 +90,11 @@ Examples: `getAutotaskClient()`, `getMsgraphClient()`, `getDattoRmmClient()`, - `lib/services/sync-scheduler.ts` — node-cron singleton. **Self-initializes on first server-side import** (side effect at the bottom of the file). Schedules live in DB, admin-editable at `/admin`. -- Webhooks (`/api/webhooks/...`, `/api/zabbix/webhook`) are public per - `middleware.ts`; they verify HMAC themselves. +- Webhooks (`/api/webhooks/...`, `/api/zabbix/webhook`, `/api/rmm/loglift`) are + public per `middleware.ts`; they verify HMAC or a shared header themselves. +- Analyzer worker (`lib/services/analyzer/worker.ts`) and RMM Overshell worker + (`lib/services/rmm/worker.ts`) auto-start on import in production. Same + side-effect-import caveat as the sync scheduler. ## Auth - Better Auth with magic link + TOTP 2FA + Microsoft OAuth. Roles: `user`, `admin`, @@ -97,9 +108,10 @@ Examples: `getAutotaskClient()`, `getMsgraphClient()`, `getDattoRmmClient()`, - Dev: `npm run dev` → http://localhost:3100 - Build: `npm run build` (turbopack via Next 16) - Type check: `npx tsc --noEmit --pretty` -- Tests: `npm test` (vitest) — currently scoped to `lib/services/analyzer/**` only. - No CI yet; tests are local-only. Other parts of the codebase have no tests — - if you touch them, type-check is the only safety net. +- Tests: `npm test` (vitest) — covers `lib/services/analyzer/**`, + `lib/services/rmm/**`, `lib/services/b2/**`, and `lib/services/analyzer/ + link-discovery.test.ts`. Other parts of the codebase have no tests — if you + touch them, type-check is the only safety net. No CI yet; tests are local-only. - Docker: `docker compose up` from repo root. Postgres applies `migrations/*.sql` on init only (existing volumes won't re-run them). @@ -110,15 +122,37 @@ Examples: `getAutotaskClient()`, `getMsgraphClient()`, `getDattoRmmClient()`, - New SQL: numbered migration; never edit a committed one. - Long-form per-feature documentation belongs in `docs/`. Don't duplicate it here. +## Operator config + +- `INTEGRATIONS_DISABLED` — comma- or space-separated list of integration + keys (or aliases) to suppress from `/status` and the top-bar status light. + Disabled entries render muted, don't count toward failure summaries, and + don't flag the rollup. Set in `.env` and restart. Aliases: + `sentinelone` → `s1`, `datto` → `datto_rmm`, `it-glue` → `itglue`, + `ms-graph` → `msgraph`. Live auth checks still run (so logs still + surface the underlying state) but the UI ignores the result. + ## Watch out for - A `.env` file is committed to the repo. Treat secrets as potentially real; don't log/echo them, and flag this if it comes up. - Duplicate migration numbers exist (002, 004, 009) — alphabetical apply order. -- Sync scheduler runs as a side effect of importing `sync-scheduler.ts` on the - server. Be careful adding eager imports of that module. +- Sync scheduler, analyzer worker, and RMM worker all auto-start as side effects + of being imported on the server. Don't eager-import them from hot paths or + shared utilities. +- Analyzer LLM provider is per-request (`anthropic` | `openrouter`). The + idempotency `content_hash` is provider-scoped — the same ticket can have one + Claude row and one OpenRouter row. +- Analyzer cost ceiling: Stage 4 (Opus) skipped above $2.00 estimated cost; the + analysis is flagged for human review. +- IT Glue results destined for an LLM **must** go through + `lib/services/analyzer/itglue-search.ts` (redacted). Don't pipe raw client + output into a prompt. ## Useful existing docs +- `ARCHITECTURE.md` — runtime, data flow, workers, analyzer pipeline (read first) +- `DESIGN.md` — UI tokens, nav IA, component conventions, current cleanup backlog - `AUTOTASK_API_GUIDE.md`, `ADDIGY_API_GUIDE.md` — credential setup - `POSTGRES_SYNC_SETUP.md`, `DOCKER_README.md` - `PULSE_DATABASE_SKILL.md` — diagnostic queries -- `docs/` — sync behavior, webhook setup, workflow editor, per-integration guides +- `docs/` — sync behavior, webhook setup, workflow editor, analyzer runbook, + RMM Overshell + LogLift specs, IT Glue audit spec, per-integration guides diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..3408a98 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,396 @@ +# Pulse — Design System + +This file documents how the UI is put together today: the tokens, the +component vocabulary, the navigation IA, and the patterns each page is +expected to follow. It also calls out the rough edges that the +`nav-design-improvements` branch exists to address — treat the "Open +improvements" section as the working backlog for visual/UX cleanup. + +System overview lives in `ARCHITECTURE.md`. Per-feature notes live in `docs/`. + +## 1. Design principles + +- **Information density over decoration.** Pulse is an internal operations + console. Tables, dashboards, and admin tools win over hero spacing. +- **One source of truth per pattern.** If shadcn/ui has a primitive, use it — + don't re-implement (`` raw, custom skeletons, ad-hoc dropdowns). +- **Tokens, not hex.** Colors come from CSS variables in `app/globals.css`, + not Tailwind's full color palette. The same goes for radius and spacing. +- **Roles shape the nav, not the layout.** Hide items the user can't access; + don't restructure the page shell per role. +- **Read-first, write-confirmed.** Destructive or external-effect actions + (RMM execute, IT Glue write) live behind explicit dialogs with confirmation, + not inline buttons. + +## 2. Tokens + +App tokens are defined in `app/globals.css` (shadcn defaults). The Wulf +brand layer in `app/styles/brand.css` is imported last and repoints +`--primary` / `--accent` / `--ring` / `--chart-2` to the standards-guide +blue (`#0075AD`) and provides Wulf gray ramps (`--wulf-gray-100/300/700`). +Tailwind 4 reads them via `@theme inline`; never hard-code colors or +radii in components. + +### Colors (OKLch) + +| Token | Light | Dark | Use | +|---|---|---|---| +| `--background` / `--foreground` | white / near-black | near-black / near-white | Page surface and primary text | +| `--card` / `--card-foreground` | white | `oklch(0.205 0 0)` | Card surfaces | +| `--primary` / `--primary-foreground` | logo blue `oklch(0.55 0.16 220)` | brighter blue `oklch(0.62 0.17 220)` | Primary actions, active nav, focus ring | +| `--secondary` / `--muted` | `oklch(0.97 0 0)` | `oklch(0.269 0 0)` | Subtle surfaces, table headers | +| `--muted-foreground` | `oklch(0.556 0 0)` | `oklch(0.708 0 0)` | Secondary text, descriptions | +| `--accent` | same as primary | same as primary | Hover/selection accents | +| `--destructive` | `oklch(0.577 0.245 27.325)` | `oklch(0.704 0.191 22.216)` | Errors, destructive buttons | +| `--border` / `--input` | `oklch(0.922 0 0)` | `oklch(1 0 0 / 10%)` | Borders, input outlines | +| `--ring` | logo blue | brighter blue | Focus ring | +| `--chart-1` … `--chart-5` | distinct hues | distinct hues | Recharts series | +| `--sidebar*` | mirrors page tokens | mirrors page tokens | Reserved for a future sidebar nav | + +Status hues outside this set (success greens, warning ambers, info blues) +should still go through Tailwind's named palette at `-500` or `-600` and use +the muted background pair `bg-{hue}-500/15 text-{hue}-600` for badges so the +contrast stays acceptable in both modes. + +### Radius + +`--radius: 0.625rem`. Derived: `radius-sm` (`-4px`), `radius-md` (`-2px`), +`radius-lg` (=), `radius-xl` (`+4px`). Use `rounded-md` / `rounded-lg` — +don't introduce custom radii. + +### Type + +Brand mandate (per `docs/StandardsGuide (1).pdf`, 2013): Helvetica / +Arial, Bold for headers and Light for the tagline. **As of 2026-05 the +app uses IBM Plex Sans instead** — same spirit (engineered sans, Light +weight available), reliably hosted, and pairs with Plex Mono for +numerics. Helvetica / Arial remain in the fallback chain. + +- Sans: **IBM Plex Sans** via `next/font/google`, weights 300/400/500/600/700. + Wired via `--font-plex-sans` in `app/layout.tsx`. +- Mono: **IBM Plex Mono** via `next/font/google`, weights 400/500/600. + Used for numerics, IDs, timestamps — not for body text. + +Type scale: stick to Tailwind defaults. Reserve `text-2xl font-bold +tracking-tight` for page titles (matches `PageHeader`); use `text-lg +font-semibold` for card titles and `text-sm text-muted-foreground` for +descriptions. Numerics go through the `.num` / `.num-lg` / `.num-xl` +utilities defined in `app/styles/brand.css`. + +### Dark mode + +CSS-class strategy (`.dark` on ``). Toggled by `ThemeToggle` in the +top bar; persisted via `next-themes`. Component-level dark variants should be +unnecessary if you stick to tokens. + +## 3. Layout + +### Page shell + +``` +┌─ (sticky h-16, backdrop blur, z-50) ─┐ +├─ (optional, bordered, container-aligned) +└─
(container mx-auto px-6 py-6) + ├─ Cards / sections, separated by space-y-6 + └─ … +``` + +`PageHeader` lives at the bottom of `components/navigation/app-navigation.tsx`. +It owns the title (`h1.text-2xl.font-bold.tracking-tight`), description +(`text-muted-foreground`), optional breadcrumbs (`/`-separated), and an +actions slot. + +### Container & spacing rules + +| Rule | Value | +|---|---| +| Horizontal container | `container mx-auto px-6` | +| Vertical page padding | `py-6` (default), `py-8` only on auth/landing | +| Section gap | `space-y-6` for stacked sections, `gap-6` for grid layouts | +| Card body padding | shadcn default — don't override unless wrapping a table (`p-0`) | +| Item-level gap | `gap-2` for inline rows, `gap-4` for form rows | + +These are the targets. Today's pages don't all conform — see "Open +improvements" below. + +## 4. Navigation IA + +The top bar (`components/navigation/app-navigation.tsx`) is the only nav. It +is sticky, full-width, and structured left-to-right as: + +1. **Brand** — logo + "Pulse" wordmark (`hidden sm:block` for the wordmark) +2. **Primary menu** (``) +3. **Right-side controls** — `` + +### Items today + +| Label | Route | Visibility | +|---|---|---| +| Dashboard | `/` | All | +| Configuration Items | `/configuration-items` | All | +| Backup Status ▾ | submenu | All | +| → Backup Status | `/backup-status` | | +| → RPO Comparison | `/veeam-comparison` | | +| → Ticket Analysis | `/veeam-analysis` | | +| Engagement ▾ | submenu | super-admin | +| → Overview | `/engagement` | | +| → Employee Profile | `/engagement/profile` | | +| Analyzer ▾ | submenu | All | +| → Browse Tickets | `/analyzer/tickets` | | +| → Aggregate Reports | `/analyzer/reports` | | +| → Needs Review | `/analyzer/queue` | | +| → IT Glue — Applications | `/analyzer/itglue/applications` | | +| → IT Glue — Configurations | `/analyzer/itglue/configurations` | | +| Admin | `/admin` | super-admin | + +Role gating is hardcoded against `session.user.role`. Hidden items are not +rendered (no greyed-out variants). + +`/admin` is the entry tile for everything admin-only — sync schedules, rules, +mappings, RMM Overshell, IT Glue write log, device-link conflicts. Sub-pages +don't appear in the top nav; they're reached from the admin landing grid. + +### Mobile + +`AppNavigation` returns `null` for any path under `/mobile` — kiosk and field +flows have their own shell. The desktop nav doesn't currently collapse to a +hamburger; under `sm` the brand wordmark hides and the menu items wrap. +Mobile-friendly behavior on small viewports is on the improvements list. + +## 5. Components + +### shadcn/ui primitives (in `components/ui/`) + +| Available | Used heavily | Rare / underused | +|---|---|---| +| `accordion` | | yes (collapsible filters could use it) | +| `alert`, `alert-dialog` | yes (errors, confirmations) | | +| `badge` | yes (status, counts) | | +| `button` | yes | | +| `calendar`, `popover` | | engagement profile | +| `card`, `card-header/title/content` | yes (page sections) | | +| `checkbox`, `switch` | yes | | +| `collapsible` | yes (CI page) | | +| `dialog`, `alert-dialog` | yes (modals) | | +| `dropdown-menu` | yes (actions) | | +| `form` (react-hook-form bridge) | admin/auth only | | +| `input`, `label`, `textarea` | yes | | +| `multi-select` | analyzer filters | | +| `navigation-menu` | top bar only | | +| `progress` | | yes (could replace ad-hoc bars) | +| `scroll-area` | | yes | +| `select` | yes | | +| `separator` | yes | | +| `skeleton` | yes (loading states) | | +| `slider` | rare | | +| `sonner` (toast) | yes — `richColors`, top-right | | +| `table` | **inconsistent** — many pages use raw `
` | | +| `tabs` | yes (Engagement, CI, DetailModal) | | + +### Feature components + +| Path | Purpose | +|---|---| +| `components/navigation/app-navigation.tsx` | Top nav + `PageHeader` | +| `components/admin/DataTable.tsx` | Custom paginated/sortable table (not @tanstack) | +| `components/admin/DetailModal.tsx` | Ticket detail shell, tabs (status/priority maps now in `lib/status-registry.ts`) | +| `components/branding/wulf-mark.tsx` | `` — W glyph or full wordmark | +| `components/branding/tagline-footer.tsx` | "Don't be afraid to cry · Wulf Consulting" footer line | +| `components/dashboard/kpi-card.tsx` | KpiCard with delta indicator and tonal left border | +| `components/navigation/page-header.tsx` | `` — title, breadcrumbs, actions; supports `accent` and `watermark` | +| `components/navigation/status-indicator.tsx` | Top-bar StatusLight that links to `/status` | +| `components/ui/empty-state.tsx` | Shared zero-data placeholder | +| `components/ui/status-badge.tsx` | Small rounded pill driven by `lib/status-registry.ts` | +| `components/ui/status-light.tsx` | 8 px square indicator (5 states, 3 sizes) | +| `components/admin/SyncScheduler.tsx` | Schedule editor on `/admin` | +| `components/analyzer/analyze-button.tsx` | Trigger analysis from a ticket | +| `components/analyzer/share-modal.tsx` | Email share dialog | +| `components/analyzer/provider-toggle.tsx` | Anthropic/OpenRouter switch | +| `components/analyzer/related-tickets-panel.tsx` | Bundle members on analysis page | +| `components/analyzer/itglue-suggestions-panel.tsx` | Inline IT Glue cross-refs | +| `components/rmm/rmm-dispatch-dialog.tsx` | Pick + execute RMM script | +| `components/rmm/rmm-script-picker.tsx` | Script registry browser | +| `components/rmm/rmm-execution-stream.tsx` | Live tail of an execution | +| `components/configuration-items/config-item-modal.tsx` | CI detail shell | + +Anything not in those two directories — and not a one-off page-local +subcomponent — should probably move there. + +## 6. Icons + +`lucide-react`, imported per-icon. Conventions: + +- Inline with text: `h-4 w-4` (16 px), with `mr-2` if leading. +- Standalone tile/card icons: `h-6 w-6`. +- Admin landing tile icons: `h-8 w-8` plus a muted color (`text-muted-foreground`). +- Color: inherit from text; status icons (`text-orange-600`, `text-green-600`) + only when they carry semantic meaning, not for decoration. + +Mixed `size-4` vs `h-4 w-4` exists today. Prefer `h-4 w-4` to match shadcn. + +## 7. Tables, modals, forms + +### Tables + +There are two patterns in the codebase. Pick the leftmost that fits: + +1. **`components/admin/DataTable.tsx`** — paginated, sortable, searchable. + Backed by `@tanstack/react-table` v8 in manual mode. Use this for any + list ≥ 25 rows or where users need to filter / sort. Pass + `getRowCanExpand` + `renderSubRow` for drill-down rows. +2. **shadcn `Table`** primitive in `components/ui/table.tsx` — for short + static lists wrapped in a `` with ``. + +Raw `
` markup is no longer in use anywhere under `app/`. If you find +yourself reaching for it, lift the layout into one of the two primitives. + +### Modals & dialogs + +- **`Dialog`** — most cases (forms, confirmations, RMM dispatch). +- **`AlertDialog`** — destructive confirmations only. +- **`DetailModal`** — tabbed read-only deep-dive on a ticket. Hardcoded + status/priority color map; if you need that map elsewhere, lift it out + rather than copy. +- A "panel" (e.g., `related-tickets-panel`) is a card laid out like a side + panel — it is not a modal and shouldn't trap focus. + +### Forms + +- **react-hook-form + Zod resolver** with the shadcn `Form` primitive — used + on auth and admin forms (invites, role edits). Use this for any new form + with ≥ 3 fields or any field that needs validation. +- **Controlled inputs** (`useState` + `onChange`) — acceptable for one-off + filter bars. Don't introduce a third pattern. + +Error display today is inconsistent (toast vs inline). New forms should +display field errors inline via `` and use toasts only for +submit-time outcomes. + +## 8. Feedback + +- **Toasts** — `sonner` configured top-right with `richColors` (`app/layout.tsx`). + Use `toast.success` / `toast.error` / `toast.info`. Don't render error + banners inside the page when a toast fits. +- **Loading** — `` for content placeholders, `` from + lucide with `animate-spin` for inline button spinners. Don't roll a custom + spinner div. +- **Empty states** — today: a centered `text-muted-foreground` line. Aim: + a small icon (lucide), a one-line headline, an optional CTA. There is no + shared `EmptyState` component yet — adding one is on the improvements list. + +## 9. Charts + +`recharts`, themed via the `--chart-1` … `--chart-5` CSS variables (Tailwind +exposes them as `text-chart-1` etc., so colors flip with light/dark +automatically). Common margins `{ top: 5, right: 8, bottom: 24, left: 0 }`. +Examples: `app/dashboard/page.tsx`, `app/engagement/page.tsx`. + +## 10. Open improvements (this branch) + +The `nav-design-improvements` branch tracks visual/UX cleanup. The list +below is the working backlog; expand as we go. + +### Navigation +- [x] ~~Top-nav "Admin" item is a flat link~~ — now a dropdown with the + seven most-used admin pages (Sync, Workflow, RMM Overshell, IT Glue + Writes, Device Conflicts, Users & roles). +- [x] ~~No user menu / avatar / sign-out in the top bar~~ — `` + now sits next to `ThemeToggle`. Shows initials, name/email, role + badge, links to `/settings` + `/settings/security`, and sign-out. +- [x] ~~No mobile-collapsed (hamburger) version~~ — `` in a + Sheet. Reuses the desktop nav config so IA stays in sync. Desktop nav + hides under `md`. +- [x] ~~Submenu width is fixed~~ — now content-driven via `min-w-[320px] + max-w-[440px]`, single column with denser rows, so 7-item Admin and + 5-item Analyzer menus fit without forced two-column awkwardness. +- [x] ~~Active-route highlighting on submenu items~~ — every top-bar item + (flat or trigger) marks active state with a 2 px Wulf-blue underline + that echoes the PageHeader rule. Active children inside dropdowns get + a tinted `bg-primary/10` background. + +### Page shell +- [x] ~~Adopt `PageHeader` everywhere~~ — `/dashboard`, `/status`, all + `/admin/*` sub-pages, `/addigy-devices`, `/analyzer/ticket/[…]`, + `/analyzer/analysis/[…]` now use it. A handful of pages still need + it (kiosk, settings, sentinelone/* — low priority). +- [ ] Standardize container + padding (`container mx-auto px-6 py-6`). Pages + using `px-4`, `py-8`, `max-w-2xl` etc. should justify the deviation. +- [ ] Standardize section spacing (`space-y-6` between cards; pick `gap-6` + for grids). +- [x] ~~Add breadcrumbs to ticket / analysis detail routes~~. Still open + for `/configuration-items/[id]` and `/analyzer/itglue/{applications, + configurations}/[id]`. + +### Components +- [x] ~~Build a shared ``~~ — `components/ui/empty-state.tsx`. +- [x] ~~Build a shared `` that consumes the priority/status maps + from `DetailModal`~~ — registry in `lib/status-registry.ts`, + badge in `components/ui/status-badge.tsx`, DetailModal migrated. +- [x] ~~Extract `PageHeader` and add `accent` + `watermark` props~~ — + `components/navigation/page-header.tsx`. Adopted on `/dashboard` + and `/status`. +- [x] ~~Replace raw `
` markup on admin and Addigy pages with shadcn + `Table`~~ — Addigy, /status, all admin/sync/* pages, backup-status, + engagement/profile, veeam-comparison migrated. Only `/veeam-analysis` + remains (DataTable territory; filter + expand + paginate). +- [x] ~~Replace raw `` on `/addigy-devices`~~ — + done; also caught one in `/admin/sync/mimecast`. +- [x] ~~/veeam-analysis raw `
`~~ — migrated to shadcn `Table` + primitive. The page keeps its own pagination + category filter + row + expansion (DataTable doesn't support row expansion yet, so a deeper + DataTable migration is folded into the long-term decision below). +- [x] ~~Decide on `DataTable` long-term~~ — **migrated to + `@tanstack/react-table` v8** in manual mode. The external API stays + stable (existing data-browser pages keep working without changes); + internally TanStack drives sort + expansion. New optional props for + consumers: `getRowCanExpand` and `renderSubRow` enable + expandable-row patterns (useful for `/veeam-analysis`-style + drill-downs). + +### Status & dashboard split (2026-05-03) +- [x] Move integration health + sync health off `/dashboard` onto a + dedicated `/status` route. Top-bar `` links there. +- [x] Rebuild `/dashboard` around KPI cards (today snapshot + needs + attention) and recent activity. Drop the integration / sync + cards. +- [x] ~~Add queue × priority heatmap to `/dashboard`~~ — `QueueHeatmap` + backed by `/api/dashboard/trends`. +- [x] ~~Add 30-day volume + mean resolution charts~~ — `VolumeTrend` and + `ResolutionTrend` components, recharts, brand-blue series. +- [x] ~~Add active engineers panel~~ — `ActiveEngineers` (today's hours + logged, ticket touch count). +- [x] ~~Worker pulse section on `/status`~~ — analyzer / RMM / sync + scheduler heartbeats via `/api/status/workers` and `WorkerPulse`. + +### Tokens & theming +- [ ] Audit places that hard-code Tailwind palette colors (`text-orange-600`, + `bg-blue-500/15`) and either keep them as semantic status colors or + move them behind a token. +- [ ] Verify dark-mode contrast on status badges and chart legends; the 10%- + opacity borders in dark mode are subtle and may need lifting. + +### Loading & empty +- [x] ~~Standardize Skeleton heights~~ — helpers in + `components/ui/skeleton-helpers.tsx`: `SkeletonRow`, `SkeletonRows`, + `SkeletonCard`, `SkeletonChart`, `SkeletonHeader`, `SkeletonTable`. +- [ ] Adopt the helpers across pages (still scattering `h-12` / `h-24` in + pages built before the helpers landed). +- [ ] Loading shells should match the post-load layout — skeletons inside + Cards, not a single full-width bar. + +### Mobile +- [x] ~~CI filter bar overflows on small viewports~~ — company selector + now wraps and shrinks; the stat pill flows below. +- [x] ~~Analyzer multi-select dropdowns clip on narrow widths~~ — + Popover gets `max-w-[calc(100vw-1rem)]` and `collisionPadding={8}`. +- [ ] Tables horizontally scroll without a sticky first column; consider + responsive card-list fallbacks for narrow screens. + +## 11. When in doubt + +- Use a token, not a hex value. +- Use the shadcn primitive, not a custom one. +- Match the surrounding page's spacing scale rather than introducing a new + one. +- If the same thing exists in two shapes (e.g., raw vs DataTable), pick the + shape this doc documents and migrate the other. diff --git a/app/addigy-devices/page.tsx b/app/addigy-devices/page.tsx index 34f1585..f299551 100644 --- a/app/addigy-devices/page.tsx +++ b/app/addigy-devices/page.tsx @@ -2,6 +2,23 @@ import { useState, useEffect } from 'react'; import { AddigyDevice } from '@/lib/types/addigy'; +import { PageHeader } from '@/components/navigation/page-header'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Card, CardContent } from '@/components/ui/card'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Button } from '@/components/ui/button'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { EmptyState } from '@/components/ui/empty-state'; +import { StatusBadge } from '@/components/ui/status-badge'; +import { Check, X, Laptop, RefreshCw } from 'lucide-react'; export default function AddigyDevicesPage() { const [devices, setDevices] = useState([]); @@ -10,21 +27,19 @@ export default function AddigyDevicesPage() { const [filterOnline, setFilterOnline] = useState(false); useEffect(() => { - fetchDevices(); + void fetchDevices(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [filterOnline]); - const fetchDevices = async () => { + async function fetchDevices() { setLoading(true); setError(null); - try { const url = filterOnline ? '/api/addigy-devices?online=true' : '/api/addigy-devices'; - - const response = await fetch(url); - const result = await response.json(); - + const res = await fetch(url, { cache: 'no-store' }); + const result = await res.json(); if (result.success) { setDevices(result.data); } else { @@ -35,159 +50,137 @@ export default function AddigyDevicesPage() { } finally { setLoading(false); } - }; + } return ( -
-
-

Addigy Devices

-
- - -
-
+ <> + + + + + } + /> - {error && ( -
- Error: {error} -
- )} +
+ {error && ( + + Failed to load + {error} + + )} - {loading ? ( -
-
-

Loading devices...

-
- ) : ( - <> -
- Found {devices.length} device{devices.length !== 1 ? 's' : ''} -
- -
-
-
- - - - - - - - - - - - - {devices.map((device) => ( - - - - - - -
- Device Name - - Model - - OS Version - - Current User - - Status - - Free Disk - - Security -
-
- {device['Device Name']} -
-
- {device['Serial Number'] || 'N/A'} -
-
- {device['Device Model Name'] || 'Unknown'} - - {device['MAC OS X Version'] || - device['iOS Version'] || - 'N/A'} - - {device['Current User'] || 'N/A'} - - - {device.online ? 'Online' : 'Offline'} - - - {device['Free Disk Percentage'] !== undefined ? ( -
- - {device['Free Disk Percentage']}% - + + + {loading ? ( +
+ + + +
+ ) : devices.length === 0 ? ( +
+ +
+ ) : ( + + + + Device + Model + OS + Current user + Status + Free disk + Security + + + + {devices.map((device) => { + const freePct = device['Free Disk Percentage']; + const freeTone = + freePct === undefined + ? 'text-muted-foreground' + : freePct < 20 + ? 'text-destructive' + : freePct < 40 + ? 'text-amber-600 dark:text-amber-400' + : 'text-emerald-600 dark:text-emerald-400'; + return ( + + +
{device['Device Name']}
+
+ {device['Serial Number'] || '—'}
- ) : ( - 'N/A' - )} - -
- - ))} - -
-
- - FW: {device['Firewall Enabled'] ? '✓' : '✗'} - - - FV: {device['FileVault Enabled'] ? '✓' : '✗'} - -
-
-
- - - )} - + + {device['Device Model Name'] || 'Unknown'} + + {device['MAC OS X Version'] || device['iOS Version'] || '—'} + + {device['Current User'] || '—'} + + + {device.online ? 'Online' : 'Offline'} + + + + {freePct !== undefined ? `${freePct}%` : '—'} + + +
+ + +
+
+ + ); + })} + +
+ )} + + + + + ); +} + +function SecurityFlag({ label, enabled }: { label: string; enabled: boolean }) { + return ( + + {enabled ? : } + {label} + ); } diff --git a/app/admin/audit-log/page.tsx b/app/admin/audit-log/page.tsx index 449351c..8587b74 100644 --- a/app/admin/audit-log/page.tsx +++ b/app/admin/audit-log/page.tsx @@ -1,20 +1,23 @@ import { Suspense } from "react"; import { AuditLogTable } from "@/components/admin/audit/audit-log-table"; import { Skeleton } from "@/components/ui/skeleton"; +import { PageHeader } from '@/components/navigation/page-header'; export default function AuditLogPage() { return ( -
-
-

Audit Log

-

- View system activity and security events -

+ <> + +
+ }> + +
- }> - - -
+ ); } diff --git a/app/admin/data-browser/page.tsx b/app/admin/data-browser/page.tsx index 93d0419..0f3ac2a 100644 --- a/app/admin/data-browser/page.tsx +++ b/app/admin/data-browser/page.tsx @@ -5,6 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Button } from '@/components/ui/button'; import { Database, Table2, Users, Ticket, CheckSquare, FolderKanban, Wrench, Tag, ArrowLeft, Home, Clock, MessageSquare } from 'lucide-react'; import Link from 'next/link'; +import { PageHeader } from '@/components/navigation/page-header'; const entities = [ { name: 'Companies', icon: Users, path: '/admin/data-browser/companies', description: 'View all companies' }, @@ -23,23 +24,24 @@ const entities = [ export default function DataBrowserPage() { return ( -
-
- - - - -
-

Database Browser

-

Inspect synced data from PostgreSQL

-
-
- -
+ <> + + + + } + /> +
+
{entities.map((entity) => { const Icon = entity.icon; return ( @@ -56,7 +58,8 @@ export default function DataBrowserPage() { ); })} +
-
+ ); } diff --git a/app/admin/device-link-conflicts/page.tsx b/app/admin/device-link-conflicts/page.tsx index 3096370..e321313 100644 --- a/app/admin/device-link-conflicts/page.tsx +++ b/app/admin/device-link-conflicts/page.tsx @@ -15,6 +15,7 @@ import { SelectValue, } from '@/components/ui/select'; import { CheckCircle2, AlertTriangle, Loader2 } from 'lucide-react'; +import { PageHeader } from '@/components/navigation/page-header'; interface Candidate { ciId: string; @@ -105,7 +106,14 @@ export default function DeviceLinkConflictsPage() { } return ( -
+ <> + +
@@ -248,6 +256,7 @@ export default function DeviceLinkConflictsPage() { ))} -
+
+ ); } diff --git a/app/admin/display-settings/page.tsx b/app/admin/display-settings/page.tsx index 97c6789..caa195e 100644 --- a/app/admin/display-settings/page.tsx +++ b/app/admin/display-settings/page.tsx @@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; +import { PageHeader } from '@/components/navigation/page-header'; interface CompanyCategory { value: number; @@ -287,18 +288,15 @@ export default function DisplaySettingsPage() { } return ( -
-
-

- - Display Settings -

-

- Configure which companies appear in the Kiosk and Mobile dashboards. -

-
- -
+ <> + +
+
+
-
+ ); } diff --git a/app/admin/itglue-writes/page.tsx b/app/admin/itglue-writes/page.tsx index ddcc1e3..f80f518 100644 --- a/app/admin/itglue-writes/page.tsx +++ b/app/admin/itglue-writes/page.tsx @@ -7,6 +7,7 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { PageHeader } from '@/components/navigation/page-header'; interface WriteRow { id: string; @@ -76,7 +77,14 @@ export default function ItglueWritesPage() { }, [statusFilter]); return ( -
+ <> + +
@@ -163,6 +171,7 @@ export default function ItglueWritesPage() { )} -
+
+ ); } diff --git a/app/admin/morning-summary/page.tsx b/app/admin/morning-summary/page.tsx index b077285..d51b7c9 100644 --- a/app/admin/morning-summary/page.tsx +++ b/app/admin/morning-summary/page.tsx @@ -6,6 +6,7 @@ import { Send, RefreshCw, Trash2, Plus, CheckCircle2, XCircle, AlertTriangle, Clock, Loader2, ChevronDown, ChevronUp, ToggleLeft, ToggleRight, } from 'lucide-react'; +import { PageHeader } from '@/components/navigation/page-header'; interface WebhookConfig { id: number; @@ -210,7 +211,23 @@ export default function MorningSummaryPage() { } return ( -
+ <> + + + + + } + /> +
{/* Toast */} {toast && (
@@ -219,21 +236,6 @@ export default function MorningSummaryPage() {
)} - {/* Header */} -
-
-

☀️ Morning NOC Summary

-

Scheduled 6:30 AM Mon–Fri · Posts to Teams channels via webhook

-
-
- - -
-
- {/* Last Run Stats */} {latestSummary && (
@@ -462,6 +464,7 @@ export default function MorningSummaryPage() {
)} -
+
+ ); } diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 3d452a1..5e9f330 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; +import { PageHeader } from '@/components/navigation/page-header'; import { RefreshCw, Network, @@ -273,15 +274,15 @@ export default function AdminIndexPage() { ]; return ( -
-
-

Admin

-

- Sync, mappings, workflows, reporting, and tooling. -

-
- -
+ <> + +
+
{sections.map((section) => ( @@ -315,7 +316,8 @@ export default function AdminIndexPage() { ))} +
-
+ ); } diff --git a/app/admin/qbo/page.tsx b/app/admin/qbo/page.tsx index 544d815..28e35a1 100644 --- a/app/admin/qbo/page.tsx +++ b/app/admin/qbo/page.tsx @@ -7,6 +7,7 @@ import { CheckCircle2, XCircle, AlertTriangle, RefreshCw, Loader2, Link2, Link2Off, FileText, CreditCard, Building2, ArrowDownToLine, BarChart3, } from 'lucide-react'; +import { PageHeader } from '@/components/navigation/page-header'; interface QboStatus { tokenStatus: 'valid' | 'expired' | 'missing'; @@ -118,19 +119,20 @@ function QboPageInner() { }[status?.tokenStatus ?? 'missing']; return ( -
- {/* Header */} -
-
-

QuickBooks Online

-

Sync invoices, payments, deposits, transactions and financial reports

-
- -
- + <> + + + Refresh + + } + /> +
{/* Banner */} {banner && (
)}
-
+
+ ); } diff --git a/app/admin/rmm-overshell/page.tsx b/app/admin/rmm-overshell/page.tsx index be09b8a..5d42d61 100644 --- a/app/admin/rmm-overshell/page.tsx +++ b/app/admin/rmm-overshell/page.tsx @@ -8,6 +8,7 @@ import { Skeleton } from '@/components/ui/skeleton'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Loader2, RefreshCw, Terminal } from 'lucide-react'; import { toast } from 'sonner'; +import { PageHeader } from '@/components/navigation/page-header'; interface Settings { overshellComponentUid: string | null; @@ -94,7 +95,14 @@ export default function RmmOvershellAdminPage() { } return ( -
+ <> + +
@@ -267,6 +275,7 @@ export default function RmmOvershellAdminPage() { )} -
+
+ ); } diff --git a/app/admin/roles/page.tsx b/app/admin/roles/page.tsx index 0ef05ed..77dcae1 100644 --- a/app/admin/roles/page.tsx +++ b/app/admin/roles/page.tsx @@ -1,15 +1,18 @@ import { RoleTable } from "@/components/admin/roles/role-table"; +import { PageHeader } from '@/components/navigation/page-header'; export default function RolesPage() { return ( -
-
-

Role Management

-

- Manage roles and their permissions -

+ <> + +
+
- -
+ ); } diff --git a/app/admin/settings/page.tsx b/app/admin/settings/page.tsx index 5056d5f..7a9afb1 100644 --- a/app/admin/settings/page.tsx +++ b/app/admin/settings/page.tsx @@ -8,6 +8,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { PageHeader } from '@/components/navigation/page-header'; export default function SettingsPage() { const [settings, setSettings] = useState>({}); @@ -62,14 +63,14 @@ export default function SettingsPage() { } return ( -
-
-

Settings

-

- Configure application settings -

-
- + <> + +
Microsoft @@ -168,6 +169,7 @@ export default function SettingsPage() { )}
-
+
+ ); } diff --git a/app/admin/sync/datto-rmm/page.tsx b/app/admin/sync/datto-rmm/page.tsx index 87041a1..2f1cee9 100644 --- a/app/admin/sync/datto-rmm/page.tsx +++ b/app/admin/sync/datto-rmm/page.tsx @@ -4,6 +4,15 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { StatusBadge } from '@/components/ui/status-badge'; import { ArrowLeft, Activity, History, Monitor, Loader2, RefreshCw, ExternalLink, Server, Wifi, WifiOff, AlertTriangle, Bell, XCircle, CheckCircle2, Clock, @@ -108,39 +117,38 @@ function HistoryTab({ refreshKey }: { refreshKey: number }) { return (
- - - - - - - - - - - +
TypeStatusRecordsStartedDuration
+ + + Type + Status + Records + Started + Duration + + + {rows.map((row: any, i: number) => { const dur = row.completed_at && row.started_at ? Math.round((new Date(row.completed_at).getTime() - new Date(row.started_at).getTime()) / 1000) : null; + const tone = row.status === 'completed' ? 'ok' : row.status === 'failed' ? 'error' : 'warn'; return ( - - - - - - - + + {row.sync_type} + + {row.status} + + {row.records_added ?? 0} + {fmtDate(row.started_at)} + + {dur != null ? `${dur}s` : '—'} + + ); })} - -
{row.sync_type} - {row.status} - {row.records_added ?? 0}{fmtDate(row.started_at)}{dur != null ? `${dur}s` : '—'}
+ +
); } diff --git a/app/admin/sync/duo/page.tsx b/app/admin/sync/duo/page.tsx index 43dfe8b..9ec527d 100644 --- a/app/admin/sync/duo/page.tsx +++ b/app/admin/sync/duo/page.tsx @@ -3,6 +3,14 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; import { RefreshCw, ArrowLeft, Loader2, CheckCircle2, AlertTriangle, Shield, Users, Smartphone, ScrollText, Layers, AppWindow, ChevronDown, ChevronUp, ShieldOff, ShieldAlert, ShieldX } from 'lucide-react'; interface DuoStatus { @@ -226,37 +234,37 @@ export default function DuoSyncPage() {

Child Accounts ({childAccounts.length})

- - - - - - - - - - - +
Account NameUsersIntegrationsMatched CompanyLast Sync
+ + + Account name + Users + Integrations + Matched company + Last sync + + + {childAccounts.map(a => ( - - - - - - - + + {fmtDate(a.synced_at)} + ))} - -
{a.name}{a.user_count}{a.integration_count} + + {a.name} + {a.user_count} + {a.integration_count} + {a.autotask_company_name ? ( - + {a.autotask_company_name} ) : ( )} - {fmtDate(a.synced_at)}
+ +
@@ -294,40 +302,38 @@ function FlaggedUsersTable({ title, description, users, icon, borderColor, bgCol

{description}

-
- - - - - - - - - - - - - {users.map(u => ( - - - - - - - - - ))} - -
UserEmailAccountEnrolledLast LoginNotes
-
{u.realname || u.username}
- {u.realname &&
{u.username}
} -
{u.email || '\u2014'}{u.account_name} - {u.is_enrolled - ? - : No - } - {u.last_login ? fmtDate(u.last_login) : 'Never'}{u.notes || '\u2014'}
-
+ + + + User + Email + Account + Enrolled + Last login + Notes + + + + {users.map(u => ( + + +
{u.realname || u.username}
+ {u.realname &&
{u.username}
} +
+ {u.email || '\u2014'} + {u.account_name} + + {u.is_enrolled + ? + : No + } + + {u.last_login ? fmtDate(u.last_login) : 'Never'} + {u.notes || '\u2014'} +
+ ))} +
+
); } diff --git a/app/admin/sync/itglue/page.tsx b/app/admin/sync/itglue/page.tsx index 8e0e578..e7418bd 100644 --- a/app/admin/sync/itglue/page.tsx +++ b/app/admin/sync/itglue/page.tsx @@ -4,6 +4,15 @@ import { useState, useEffect, useCallback } from 'react'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { StatusBadge } from '@/components/ui/status-badge'; import { ArrowLeft, Activity, History, BookOpen, Loader2, RefreshCw, ExternalLink, CheckCircle2, XCircle, Clock, AlertTriangle, @@ -41,17 +50,17 @@ function StatCard({ ); } -function StatusBadge({ status }: { status: string }) { - const cls = - status === 'completed' ? 'bg-green-500/15 text-green-700' : - status === 'failed' ? 'bg-red-500/15 text-red-600' : - status === 'running' ? 'bg-blue-500/15 text-blue-700' : - 'bg-yellow-500/15 text-yellow-700'; +function SyncStatusBadge({ status }: { status: string }) { + const tone = + status === 'completed' ? 'ok' : + status === 'failed' ? 'error' : + status === 'running' ? 'info' : + 'warn'; return ( - - {status === 'running' && } + + {status === 'running' && } {status} - + ); } @@ -122,33 +131,33 @@ function StatusTab({

Last Sync Breakdown - {latest.status && } + {latest.status && }

- - - - - - - - - - +
EntityRecordsDurationStatus
+ + + Entity + Records + Duration + Status + + + {latest.entities.map((e: any, i: number) => ( - - - - - - + ? + : } + + ))} - -
{e.entity}{e.recordsUpserted.toLocaleString()}{fmtDuration(e.duration)} + + {e.entity} + {e.recordsUpserted.toLocaleString()} + {fmtDuration(e.duration)} + {e.success - ? - : } -
+ +
)} @@ -167,28 +176,28 @@ function HistoryTab({ history }: { history: any[] }) { return (
- - - - - - - - - - - +
StatusTriggered ByRecordsStartedDuration
+ + + Status + Triggered by + Records + Started + Duration + + + {history.map((row: any, i: number) => ( - - - - - - - + + + {row.triggered_by ?? 'system'} + {(row.total_upserted ?? 0).toLocaleString()} + {fmtDate(row.started_at)} + {fmtDuration(row.duration_ms)} + ))} - -
{row.triggered_by ?? 'system'}{(row.total_upserted ?? 0).toLocaleString()}{fmtDate(row.started_at)}{fmtDuration(row.duration_ms)}
+ +
); } diff --git a/app/admin/sync/mimecast/page.tsx b/app/admin/sync/mimecast/page.tsx index f2c67eb..1b08a4a 100644 --- a/app/admin/sync/mimecast/page.tsx +++ b/app/admin/sync/mimecast/page.tsx @@ -11,6 +11,16 @@ import { Clock, ChevronDown, ChevronRight, Users, Search, LockKeyhole, UnlockKeyhole, PauseCircle, Building2, Check, Info, TrendingUp, ExternalLink, Eye, Trash2, } from 'lucide-react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { StatusBadge } from '@/components/ui/status-badge'; +import { Checkbox } from '@/components/ui/checkbox'; import SyncScheduler from '@/components/admin/SyncScheduler'; function fmtDate(d: string | null | undefined) { @@ -39,32 +49,24 @@ function StatCard({ label, value, sub, icon: Icon, cls }: { ); } -function StatusBadge({ status }: { status: string }) { - const cls = - status === 'delivered' ? 'bg-green-500/15 text-green-700' : - status === 'rejected' ? 'bg-red-500/15 text-red-600' : - status === 'held' ? 'bg-yellow-500/15 text-yellow-700' : - status === 'bounced' ? 'bg-orange-500/15 text-orange-700' : - status === 'spam' ? 'bg-purple-500/15 text-purple-700' : - 'bg-muted text-muted-foreground'; - return ( - - {status || '—'} - - ); +function MessageStatusBadge({ status }: { status: string }) { + const tone = + status === 'delivered' ? 'ok' : + status === 'rejected' ? 'error' : + status === 'held' ? 'warn' : + status === 'bounced' ? 'warn' : + status === 'spam' ? 'accent' : + 'inactive'; + return {status || '—'}; } -function ThreatBadge({ level }: { level: string }) { - const cls = - level === 'high' ? 'bg-red-500/15 text-red-600' : - level === 'medium' ? 'bg-orange-500/15 text-orange-700' : - level === 'low' ? 'bg-yellow-500/15 text-yellow-700' : - 'bg-muted text-muted-foreground'; - return ( - - {level || 'info'} - - ); +function ThreatLevelBadge({ level }: { level: string }) { + const tone = + level === 'high' ? 'error' : + level === 'medium' ? 'warn' : + level === 'low' ? 'pending' : + 'inactive'; + return {level || 'info'}; } // ── Status Tab ──────────────────────────────────────────────────────────────── @@ -211,30 +213,30 @@ function MessagesTab() { ) : (
- - - - - - - - - - - - +
FromToSubjectDirectionStatusSent
+ + + From + To + Subject + Direction + Status + Sent + + + {rows.map((r: any) => ( - - - - - - - - + + {r.sender_address ?? '—'} + {r.recipient_address ?? '—'} + {r.subject ?? '—'} + {r.direction ?? '—'} + + {fmtDate(r.sent_datetime)} + ))} - -
{r.sender_address ?? '—'}{r.recipient_address ?? '—'}{r.subject ?? '—'}{r.direction ?? '—'}{fmtDate(r.sent_datetime)}
+ +
)} @@ -260,32 +262,32 @@ function ThreatsTab() { return (
- - - - - - - - - - - - +
TypeLevelActorVerdictURL / FileWhen
+ + + Type + Level + Actor + Verdict + URL / File + When + + + {rows.map((r: any) => ( - - - - - - - - + + {fmtDate(r.event_datetime)} + ))} - -
{r.event_type ?? '—'}{r.actor_email ?? '—'}{r.verdict ?? '—'} + + {r.event_type ?? '—'} + + {r.actor_email ?? '—'} + {r.verdict ?? '—'} + {r.url ?? r.file_name ?? '—'} - {fmtDate(r.event_datetime)}
+ +
); } @@ -420,41 +422,44 @@ function HistoryTab() { return (
- - - - - - - - - - - - +
TypeStatusMessagesThreatsStartedDuration
+ + + Type + Status + Messages + Threats + Started + Duration + + + {rows.map((r: any, i: number) => { const dur = r.completed_at && r.started_at ? new Date(r.completed_at).getTime() - new Date(r.started_at).getTime() : null; const durStr = dur == null ? '—' : dur < 60000 ? `${Math.round(dur / 1000)}s` : `${Math.floor(dur / 60000)}m ${Math.round((dur % 60000) / 1000)}s`; - const statusCls = r.status === 'completed' ? 'bg-green-500/15 text-green-700' : r.status === 'failed' ? 'bg-red-500/15 text-red-600' : 'bg-muted text-muted-foreground'; + const statusTone = + r.status === 'completed' ? 'ok' : + r.status === 'failed' ? 'error' : + 'inactive'; const meta = typeof r.metadata === 'string' ? JSON.parse(r.metadata || '{}') : (r.metadata ?? {}); return ( - - - - - - - - + + {r.sync_type ?? '—'} + + {r.status} + + {fmtNum(meta.messagesUpserted ?? r.records_added)} + {fmtNum(meta.threatsUpserted)} + {fmtDate(r.started_at)} + {durStr} + ); })} - -
{r.sync_type ?? '—'} - {r.status} - {fmtNum(meta.messagesUpserted ?? r.records_added)}{fmtNum(meta.threatsUpserted)}{fmtDate(r.started_at)}{durStr}
+ +
); } @@ -901,74 +906,74 @@ function HeldMailTab() { : ''} -
- - - - - - - - - - - - - {filtered.map((m: any) => ( - - - - - - - - - ))} - -
DateToFromSubjectPolicy
- {new Date(m.dateReceived).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })} - -
{m.to}
-
-
{m.fromDisplay || m.from}
- {m.fromDisplay &&
{m.from}
} -
-
{m.subject || '(no subject)'}
-
- + + + Date + To + From + Subject + Policy + + + + + {filtered.map((m: any) => ( + + + {new Date(m.dateReceived).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })} + + +
{m.to}
+
+ +
{m.fromDisplay || m.from}
+ {m.fromDisplay &&
{m.from}
} +
+ +
{m.subject || '(no subject)'}
+
+ + - {m.policyInfo || m.reason || '—'} -
-
-
- - -
- {releaseErrors[m.id] && ( -
{releaseErrors[m.id]}
- )} -
-
+ ? 'error' + : 'inactive' + } + > + {m.policyInfo || m.reason || '—'} + + + +
+ + +
+ {releaseErrors[m.id] && ( +
{releaseErrors[m.id]}
+ )} +
+ + ))} + + )} @@ -1385,13 +1390,15 @@ function DeliveredAnalysisDialog({ message, onClose, onFindSimilar, allMessages
{remedMatches.map(m => (
- - - - - - - - - - - - - +
ObjectTypeOrganizationStatusRepeatsLast ActivationMessage
+ + + Object + Type + Organization + Status + Repeats + Last Activation + Message + + + {alarms.map((a: any) => ( - - - - - - - - - + + ))} - -
{a.object_computer_name || a.object_name || '—'}{a.object_type ?? '—'}{a.organization_name ?? '—'} - {a.last_activation_status ?? '—'} - {a.repeat_count ?? 0}{fmtDate(a.last_activation_time)} + + {a.object_computer_name || a.object_name || '—'} + {a.object_type ?? '—'} + {a.organization_name ?? '—'} + + + {a.last_activation_status ?? '—'} + + + {a.repeat_count ?? 0} + {fmtDate(a.last_activation_time)} + {a.last_activation_message?.trim() || '—'} -
+ +
); @@ -463,47 +480,48 @@ function RpoTab({ refreshKey }: { refreshKey: number }) {

RPO Breached ({breachedJobs.length})

- - - - - - - - - - - - +
JobOrganizationLast BackupOverdueFailure ReasonTicket
+ + + Job + Organization + Last Backup + Overdue + Failure Reason + Ticket + + + {breachedJobs.map((j: any) => { const hrs = j.hours_since_backup; const display = hrs === null ? 'Never' : hrs >= 48 ? `${Math.round(hrs / 24)}d` : `${Math.round(hrs)}h`; - const ticketPriCls = j.open_ticket?.priority_level === 'critical' ? 'bg-red-500/15 text-red-700' - : j.open_ticket?.priority_level === 'high' ? 'bg-orange-500/15 text-orange-700' - : 'bg-yellow-500/15 text-yellow-700'; + const ticketTone = + j.open_ticket?.priority_level === 'critical' ? 'error' : + j.open_ticket?.priority_level === 'high' ? 'warn' : + 'pending'; return ( - - - - - - - - + + ); })} - -
{j.job_name}{j.org_name}{fmtDate(j.last_end_time)}{display} + + {j.job_name} + {j.org_name} + {fmtDate(j.last_end_time)} + {display} + {j.failure_category ?? '—'} - + + {j.open_ticket ? ( - + {j.open_ticket.at_ticket_number} · {j.open_ticket.priority_level} - + ) : ( No ticket yet )} -
+ + )} @@ -512,30 +530,30 @@ function RpoTab({ refreshKey }: { refreshKey: number }) { Within RPO ({healthyJobs.length}) - - - - - - - - - - - +
JobOrganizationLast BackupHours AgoRPO
+ + + Job + Organization + Last Backup + Hours Ago + RPO + + + {healthyJobs.map((j: any) => ( - - - - - - - + + {j.rpo_hours}h + ))} - -
{j.job_name}{j.org_name}{fmtDate(j.last_end_time)} + + {j.job_name} + {j.org_name} + {fmtDate(j.last_end_time)} + {j.hours_since_backup !== null ? `${j.hours_since_backup}h` : '—'} - {j.rpo_hours}h
+ + )} diff --git a/app/admin/ticket-digest/page.tsx b/app/admin/ticket-digest/page.tsx index 0ee4233..ab81430 100644 --- a/app/admin/ticket-digest/page.tsx +++ b/app/admin/ticket-digest/page.tsx @@ -7,6 +7,7 @@ import { Clock, Loader2, ChevronDown, ChevronUp, BarChart3, Brain, Calendar, CalendarDays, CalendarRange, MessageSquare, Bell, Globe, ExternalLink, } from 'lucide-react'; +import { PageHeader } from '@/components/navigation/page-header'; interface DigestConfig { daily_enabled: boolean; @@ -172,7 +173,19 @@ export default function TicketDigestPage() { ); return ( -
+ <> + + Refresh + + } + /> +
{/* Toast */} {toast && (
@@ -180,21 +193,6 @@ export default function TicketDigestPage() {
)} - {/* Header */} -
-
-

- Ticket Digest Reports -

-

- LLM-analyzed ticket reports delivered to Teams — daily, weekly, and monthly -

-
- -
- {/* Generate Reports */}

@@ -451,6 +449,7 @@ export default function TicketDigestPage() {

)}
-
+ + ); } diff --git a/app/admin/users/page.tsx b/app/admin/users/page.tsx index 2e42b70..4d3beea 100644 --- a/app/admin/users/page.tsx +++ b/app/admin/users/page.tsx @@ -1,20 +1,23 @@ import { Suspense } from "react"; import { UserTable } from "@/components/admin/users/user-table"; import { Skeleton } from "@/components/ui/skeleton"; +import { PageHeader } from '@/components/navigation/page-header'; export default function UsersPage() { return ( -
-
-

User Management

-

- Manage users, roles, and permissions -

+ <> + +
+ }> + +
- }> - - -
+ ); } diff --git a/app/admin/workflow/page.tsx b/app/admin/workflow/page.tsx index 37ebadf..80da878 100644 --- a/app/admin/workflow/page.tsx +++ b/app/admin/workflow/page.tsx @@ -18,6 +18,7 @@ import { PauseCircle, } from 'lucide-react'; import { toast } from 'sonner'; +import { PageHeader } from '@/components/navigation/page-header'; interface TicketWorkflow { id: number; @@ -109,25 +110,22 @@ export default function WorkflowListPage() { }; return ( -
- {/* Header */} -
-
- -
-

Ticket Workflows

-

Automated ticket triage and classification

-
-
- - - - -
- + <> + + + + } + /> +
{/* Master Control */} @@ -284,6 +282,7 @@ export default function WorkflowListPage() {
-
+
+ ); } diff --git a/app/admin/zabbix-wan/page.tsx b/app/admin/zabbix-wan/page.tsx index 38e075d..e748a87 100644 --- a/app/admin/zabbix-wan/page.tsx +++ b/app/admin/zabbix-wan/page.tsx @@ -50,6 +50,7 @@ import { } from 'lucide-react'; import { toast } from 'sonner'; import { HostManager } from '@/components/zabbix/host-manager'; +import { PageHeader } from '@/components/navigation/page-header'; type SyncMode = 'all' | 'client' | 'site'; @@ -433,19 +434,14 @@ export default function ZabbixWanPage() { }; return ( -
- {/* Header */} -
-
-

- Zabbix WAN Monitor Setup -

-

- Create or update Zabbix hosts with WAN IPs and Autotask macros for alert routing -

-
-
- + <> + +
{/* Tabs */}
{([['sync', Globe, 'WAN Sync'], ['gaps', ShieldAlert, 'Gap Analysis'], ['correlation', Activity, 'Alert Correlation']] as const).map(([tab, Icon, label]) => ( @@ -1422,6 +1418,7 @@ export default function ZabbixWanPage() { )}
)} -
+
+ ); } diff --git a/app/analyzer/analysis/[id]/page.tsx b/app/analyzer/analysis/[id]/page.tsx index 53dccb0..8c05b72 100644 --- a/app/analyzer/analysis/[id]/page.tsx +++ b/app/analyzer/analysis/[id]/page.tsx @@ -6,6 +6,7 @@ import { AnalysisView } from '@/components/analyzer/analysis-view'; import { ItglueSuggestionsPanel } from '@/components/analyzer/itglue-suggestions-panel'; import type { PersistedAnalysis } from '@/lib/types/analyzer'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { PageHeader } from '@/components/navigation/page-header'; export default function AnalysisDetailPage({ params, @@ -36,27 +37,42 @@ export default function AnalysisDetailPage({ }; }, [id]); + const ticketNumber = analysis?.ticketNumber; return ( -
- {error && ( - - Couldn’t load this analysis - {error} - - )} - {!error && !analysis && ( -
- - - -
- )} - {analysis && ( -
- - -
- )} -
+ <> + +
+ {error && ( + + Couldn’t load this analysis + {error} + + )} + {!error && !analysis && ( +
+ + + +
+ )} + {analysis && ( +
+ + +
+ )} +
+ ); } diff --git a/app/analyzer/ticket/[ticketNumber]/page.tsx b/app/analyzer/ticket/[ticketNumber]/page.tsx index 864ae2d..91f5bff 100644 --- a/app/analyzer/ticket/[ticketNumber]/page.tsx +++ b/app/analyzer/ticket/[ticketNumber]/page.tsx @@ -12,6 +12,7 @@ import { ProviderToggle, type AnalyzerProvider, } from '@/components/analyzer/provider-toggle'; +import { PageHeader } from '@/components/navigation/page-header'; import { Sparkles, Zap } from 'lucide-react'; import type { PersistedAnalysis } from '@/lib/types/analyzer'; @@ -50,29 +51,24 @@ export default function TicketAnalyzerPage({ const latest = analyses?.[0]; return ( -
- - -
-
-

Ticket

- {ticketNumber} -
-
- - -
-
-
- -

- Click Analyze to run the AI pipeline using the - selected provider. Each provider keeps its own analysis history, - so you can compare Claude and DeepSeek output side-by-side. A run - with the same content hash on the same provider returns instantly. -

-
-
+ <> + + + + + } + /> +
{error && ( @@ -162,6 +158,7 @@ export default function TicketAnalyzerPage({ )} -
+
+ ); } diff --git a/app/api/dashboard/overview/route.ts b/app/api/dashboard/overview/route.ts index 4497da0..6c10f22 100644 --- a/app/api/dashboard/overview/route.ts +++ b/app/api/dashboard/overview/route.ts @@ -2,10 +2,11 @@ * GET /api/dashboard/overview * Single round-trip backing the new dashboard. All queries run in parallel. * + * today — KPI snapshot: opened, resolved, open total, SLA breaches * attention — counts that should pull a human's eyes * observations — recent device_observations (loglift et al.) * audits — recent endpoint_audits - * syncHealth — per-schedule last_run / last_status from sync_schedules + * syncHealth — per-schedule last_run / last_status (consumed by /status) * stats — small footer: companies, CIs, xref linkage */ @@ -20,6 +21,9 @@ export async function GET() { type Counts = { count: string }; const [ + todayRes, + yesterdayOpenedRes, + last7AvgResolvedRes, linkConflictsRes, itglueUnlinkedRes, s1UnmappedRes, @@ -31,6 +35,44 @@ export async function GET() { ciRes, xrefRes, ] = await Promise.all([ + /* today snapshot — single row, all four KPIs */ + postgresClient.query<{ + opened_today: string; + resolved_today: string; + open_total: string; + sla_breaches: string; + }>(` + SELECT + COUNT(*) FILTER (WHERE create_date::date = CURRENT_DATE)::text AS opened_today, + COUNT(*) FILTER (WHERE completed_date::date = CURRENT_DATE)::text AS resolved_today, + COUNT(*) FILTER (WHERE completed_date IS NULL)::text AS open_total, + COUNT(*) FILTER ( + WHERE completed_date IS NULL + AND due_date_time IS NOT NULL + AND due_date_time < NOW() + )::text AS sla_breaches + FROM tickets + WHERE is_deleted = false OR is_deleted IS NULL + `), + /* yesterday's opened count for the today-vs-yesterday delta */ + postgresClient.query<{ count: string }>(` + SELECT COUNT(*)::text AS count + FROM tickets + WHERE create_date::date = CURRENT_DATE - INTERVAL '1 day' + AND (is_deleted = false OR is_deleted IS NULL) + `), + /* 7-day average resolved (excluding today) for the resolved delta */ + postgresClient.query<{ avg_resolved: string }>(` + SELECT COALESCE(AVG(daily_count), 0)::text AS avg_resolved + FROM ( + SELECT completed_date::date AS d, COUNT(*) AS daily_count + FROM tickets + WHERE completed_date >= CURRENT_DATE - INTERVAL '7 days' + AND completed_date < CURRENT_DATE + AND (is_deleted = false OR is_deleted IS NULL) + GROUP BY completed_date::date + ) sub + `), postgresClient.query( `SELECT COUNT(*)::text AS count FROM device_link_review WHERE resolved_at IS NULL` ), @@ -118,7 +160,19 @@ export async function GET() { ), ]); + const today = todayRes.rows[0]; + const yesterdayOpened = parseInt(yesterdayOpenedRes.rows[0]?.count ?? '0', 10); + const last7Avg = parseFloat(last7AvgResolvedRes.rows[0]?.avg_resolved ?? '0'); + return NextResponse.json({ + today: { + openedToday: parseInt(today?.opened_today ?? '0', 10), + resolvedToday: parseInt(today?.resolved_today ?? '0', 10), + openTotal: parseInt(today?.open_total ?? '0', 10), + slaBreaches: parseInt(today?.sla_breaches ?? '0', 10), + yesterdayOpened, + last7DayAvgResolved: Math.round(last7Avg * 10) / 10, + }, attention: { linkConflicts: parseInt(linkConflictsRes.rows[0]?.count ?? '0', 10), itglueUnlinked: parseInt(itglueUnlinkedRes.rows[0]?.count ?? '0', 10), diff --git a/app/api/dashboard/trends/route.ts b/app/api/dashboard/trends/route.ts new file mode 100644 index 0000000..63d0232 --- /dev/null +++ b/app/api/dashboard/trends/route.ts @@ -0,0 +1,143 @@ +/** + * GET /api/dashboard/trends + * Operational trend data backing /dashboard's chart row + queue posture. + * + * volumeByDay — last 30 days, ticket creation count per day + * resolutionByDay — last 30 days, mean resolution hours per day completed + * queueHeatmap — open tickets grouped by (queue, priority) + * activeEngineers — top engineers today by hours logged + * + * All queries run in parallel. ~50 ms total against a warm DB. + */ + +import { NextResponse } from 'next/server'; +import { requireAuth } from '@/lib/auth-utils'; +import postgresClient from '@/lib/services/postgres-client'; + +const TREND_DAYS = 30; +const TOP_QUEUES = 10; +const TOP_ENGINEERS = 8; + +export async function GET() { + const { error } = await requireAuth(); + if (error) return error; + + const [volumeRes, resolutionRes, heatmapRes, engineersRes] = await Promise.all([ + postgresClient.query<{ d: string; count: string }>( + `WITH days AS ( + SELECT generate_series( + CURRENT_DATE - INTERVAL '${TREND_DAYS - 1} days', + CURRENT_DATE, + INTERVAL '1 day' + )::date AS d + ) + SELECT d::text AS d, + COALESCE(COUNT(t.id), 0)::text AS count + FROM days + LEFT JOIN tickets t + ON t.create_date::date = days.d + AND (t.is_deleted = false OR t.is_deleted IS NULL) + GROUP BY d + ORDER BY d`, + ), + postgresClient.query<{ d: string; avg_hours: string | null }>( + `WITH days AS ( + SELECT generate_series( + CURRENT_DATE - INTERVAL '${TREND_DAYS - 1} days', + CURRENT_DATE, + INTERVAL '1 day' + )::date AS d + ) + SELECT d::text AS d, + AVG(EXTRACT(EPOCH FROM (t.completed_date - t.create_date)) / 3600.0)::text AS avg_hours + FROM days + LEFT JOIN tickets t + ON t.completed_date::date = days.d + AND t.create_date IS NOT NULL + AND (t.is_deleted = false OR t.is_deleted IS NULL) + GROUP BY d + ORDER BY d`, + ), + postgresClient.query<{ + queue_id: number | null; + queue_label: string | null; + priority: number | null; + count: string; + }>( + `SELECT t.queue_id, + q.label AS queue_label, + t.priority, + COUNT(*)::text AS count + FROM tickets t + LEFT JOIN queues q ON q.value = t.queue_id + WHERE t.completed_date IS NULL + AND (t.is_deleted = false OR t.is_deleted IS NULL) + GROUP BY t.queue_id, q.label, t.priority + ORDER BY COUNT(*) DESC`, + ), + postgresClient.query<{ + resource_id: string; + resource_name: string; + hours: string; + tickets_touched: string; + }>( + `SELECT te.resource_id::text, + COALESCE(NULLIF(TRIM(r.first_name || ' ' || COALESCE(r.last_name, '')), ''), + r.email, + 'Resource ' || te.resource_id) AS resource_name, + SUM(te.hours_worked)::text AS hours, + COUNT(DISTINCT te.ticket_id)::text AS tickets_touched + FROM time_entries te + LEFT JOIN resources r ON r.id = te.resource_id + WHERE te.entry_date::date = CURRENT_DATE + AND te.hours_worked > 0 + GROUP BY te.resource_id, r.first_name, r.last_name, r.email + ORDER BY SUM(te.hours_worked) DESC + LIMIT ${TOP_ENGINEERS}`, + ), + ]); + + // Heatmap: top N queues by open volume × priority columns + const heatmapRows = heatmapRes.rows; + const queueTotals = new Map(); + for (const row of heatmapRows) { + if (row.queue_id == null) continue; + const t = queueTotals.get(row.queue_id) ?? { + id: row.queue_id, + label: row.queue_label ?? `Queue ${row.queue_id}`, + total: 0, + }; + t.total += parseInt(row.count, 10); + queueTotals.set(row.queue_id, t); + } + const topQueues = [...queueTotals.values()] + .sort((a, b) => b.total - a.total) + .slice(0, TOP_QUEUES); + + const heatmap = topQueues.map((q) => { + const cells: Record = {}; + for (const row of heatmapRows) { + if (row.queue_id !== q.id || row.priority == null) continue; + cells[row.priority] = (cells[row.priority] ?? 0) + parseInt(row.count, 10); + } + return { queueId: q.id, queueLabel: q.label, total: q.total, byPriority: cells }; + }); + + return NextResponse.json({ + volumeByDay: volumeRes.rows.map((r) => ({ + date: r.d, + count: parseInt(r.count, 10), + })), + resolutionByDay: resolutionRes.rows.map((r) => ({ + date: r.d, + avgHours: r.avg_hours == null ? null : Math.round(parseFloat(r.avg_hours) * 10) / 10, + })), + queueHeatmap: heatmap, + activeEngineers: engineersRes.rows.map((r) => ({ + resourceId: r.resource_id, + name: r.resource_name, + hours: Math.round(parseFloat(r.hours) * 10) / 10, + ticketsTouched: parseInt(r.tickets_touched, 10), + })), + }); +} diff --git a/app/api/status/workers/route.ts b/app/api/status/workers/route.ts new file mode 100644 index 0000000..f596b4f --- /dev/null +++ b/app/api/status/workers/route.ts @@ -0,0 +1,102 @@ +/** + * GET /api/status/workers + * Heartbeat snapshot for the three in-process workers: + * • analyzer — analyzer_jobs + * • rmm — rmm_executions + * • sync — sync_schedules / sync_history (proxy for the scheduler) + * + * For each: last activity timestamp, in-flight count, last-1h success/ + * failure totals. Cheap — just SELECT COUNT(*) FILTER queries. */ + +import { NextResponse } from 'next/server'; +import { requireAuth } from '@/lib/auth-utils'; +import postgresClient from '@/lib/services/postgres-client'; + +export interface WorkerSnapshot { + name: string; + lastActivity: string | null; + inFlight: number; + oneHour: { success: number; failure: number }; +} + +export async function GET() { + const { error } = await requireAuth(); + if (error) return error; + + const [analyzerRes, rmmRes, syncRes] = await Promise.all([ + postgresClient.query<{ + last_activity: string | null; + in_flight: string; + ok_1h: string; + fail_1h: string; + }>( + `SELECT + GREATEST(MAX(queued_at), MAX(started_at), MAX(finished_at))::text AS last_activity, + COUNT(*) FILTER (WHERE status IN ('queued','fetching','triaging','itglue','analyzing','deep_review'))::text AS in_flight, + COUNT(*) FILTER (WHERE status = 'complete' AND finished_at >= NOW() - INTERVAL '1 hour')::text AS ok_1h, + COUNT(*) FILTER (WHERE status = 'failed' AND finished_at >= NOW() - INTERVAL '1 hour')::text AS fail_1h + FROM analyzer_jobs`, + ), + postgresClient.query<{ + last_activity: string | null; + in_flight: string; + ok_1h: string; + fail_1h: string; + }>( + `SELECT + GREATEST(MAX(queued_at), MAX(started_at), MAX(completed_at))::text AS last_activity, + COUNT(*) FILTER (WHERE status IN ('queued','running'))::text AS in_flight, + COUNT(*) FILTER (WHERE status = 'complete' AND completed_at >= NOW() - INTERVAL '1 hour')::text AS ok_1h, + COUNT(*) FILTER (WHERE status IN ('failed','timeout') AND completed_at >= NOW() - INTERVAL '1 hour')::text AS fail_1h + FROM rmm_executions`, + ), + postgresClient.query<{ + last_run: string | null; + ok_1h: string; + fail_1h: string; + }>( + `SELECT + MAX(last_run)::text AS last_run, + COUNT(*) FILTER (WHERE last_status = 'success' AND last_run >= NOW() - INTERVAL '1 hour')::text AS ok_1h, + COUNT(*) FILTER (WHERE last_status = 'failed' AND last_run >= NOW() - INTERVAL '1 hour')::text AS fail_1h + FROM sync_schedules + WHERE is_enabled = true`, + ), + ]); + + const a = analyzerRes.rows[0]; + const r = rmmRes.rows[0]; + const s = syncRes.rows[0]; + + const workers: WorkerSnapshot[] = [ + { + name: 'Analyzer', + lastActivity: a?.last_activity ?? null, + inFlight: parseInt(a?.in_flight ?? '0', 10), + oneHour: { + success: parseInt(a?.ok_1h ?? '0', 10), + failure: parseInt(a?.fail_1h ?? '0', 10), + }, + }, + { + name: 'RMM Overshell', + lastActivity: r?.last_activity ?? null, + inFlight: parseInt(r?.in_flight ?? '0', 10), + oneHour: { + success: parseInt(r?.ok_1h ?? '0', 10), + failure: parseInt(r?.fail_1h ?? '0', 10), + }, + }, + { + name: 'Sync scheduler', + lastActivity: s?.last_run ?? null, + inFlight: 0, + oneHour: { + success: parseInt(s?.ok_1h ?? '0', 10), + failure: parseInt(s?.fail_1h ?? '0', 10), + }, + }, + ]; + + return NextResponse.json({ workers }); +} diff --git a/app/backup-status/page.tsx b/app/backup-status/page.tsx index 02371b6..6d9a865 100644 --- a/app/backup-status/page.tsx +++ b/app/backup-status/page.tsx @@ -12,6 +12,15 @@ import { ContractCoverageTable } from '@/components/backup/contract-coverage-tab import { RefreshCw, CheckCircle2, AlertTriangle, XCircle, Clock, WifiOff } from 'lucide-react'; import { Skeleton } from '@/components/ui/skeleton'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { StatusBadge } from '@/components/ui/status-badge'; import { RpoJobSummary } from '@/lib/services/veeam-rpo-service'; interface BackupStatusData { @@ -284,28 +293,28 @@ export default function BackupStatusPage() { {/* Job Table */}
- - - - - - - - - - - - - +
JobOrganizationLast BackupRMM DeviceStatusTicketFailure Reason
+ + + Job + Organization + Last Backup + RMM Device + Status + Ticket + Failure Reason + + + {rpo.jobs.map((job) => ( - - - - - - - - - + + ))} {rpo.jobs.length === 0 && ( - - - + + No workstation jobs found + )} - -
{job.job_name}{job.org_name}{timeAgoHours(job.hours_since_backup)} + + {job.job_name} + {job.org_name} + {timeAgoHours(job.hours_since_backup)} + {job.rmm_hostname ? (
- {job.rmm_hostname} + {job.rmm_hostname} {job.is_offline_suppressed && (
@@ -316,21 +325,21 @@ export default function BackupStatusPage() { ) : ( )} -
+ + {job.is_offline_suppressed ? ( - + Offline - + ) : job.is_breached ? ( - Breached + Breached ) : ( - Healthy + Healthy )} - + + {job.open_ticket ? ( - @@ -339,19 +348,19 @@ export default function BackupStatusPage() { ) : ( )} - + + {job.failure_category ?? '—'} -
No workstation jobs found
+ +
)} @@ -363,43 +372,43 @@ export default function BackupStatusPage() { No Autotask ticket is created while the device is offline.

- - - - - - - - - - - - - +
DeviceJobOrganizationTypeLast SeenOfflineChecked
+ + + Device + Job + Organization + Type + Last Seen + Offline + Checked + + + {offlineLog.map((row) => ( - - - - - - - - - + + {timeAgo(row.checked_at)} + ))} {offlineLog.length === 0 && ( - - - + + No offline suppressions logged yet + )} - -
{row.rmm_hostname}{row.job_name}{row.org_name} + + {row.rmm_hostname} + {row.job_name} + {row.org_name} + {row.device_type_category} - {timeAgo(row.rmm_last_seen)} + + {timeAgo(row.rmm_last_seen)} + {row.hours_offline >= 48 ? `${Math.round(row.hours_offline / 24)}d` : `${Math.round(row.hours_offline)}h`} - {timeAgo(row.checked_at)}
No offline suppressions logged yet
+ +
diff --git a/app/configuration-items/page.tsx b/app/configuration-items/page.tsx index 4ac1b8b..e870d34 100644 --- a/app/configuration-items/page.tsx +++ b/app/configuration-items/page.tsx @@ -585,9 +585,9 @@ function ConfigurationItemsContent() {
{/* Company Selector Row */} -
+
-
+
{selectedCompany && ( -
- - - PSA: {stats?.totalAutotask || 0} | RMM: {stats?.totalRmm || 0} | NMS: {stats?.totalAuvik || 0} | ARMM: {stats?.totalAddigy || 0} +
+ + + PSA {stats?.totalAutotask || 0} · RMM {stats?.totalRmm || 0} · NMS {stats?.totalAuvik || 0} · ARMM {stats?.totalAddigy || 0}
)} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 503a15a..2c8d90a 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,59 +1,48 @@ +/* /dashboard — Operations home. + * + * KPI-first. Health and sync status moved to /status (linked from the + * top-bar StatusLight). This page surfaces: + * • Today snapshot — opened, resolved, open total, SLA breaches + * • Needs attention — admin housekeeping that pulls a human's eyes + * • Recent observations + recent audits + * + * Trends (volume by day, queue heatmap) will land here next once the + * supporting endpoints exist; for now the page is intentionally minimal + * and load-fast. */ + 'use client'; import { useEffect, useState } from 'react'; -import Link from 'next/link'; +import { PageHeader } from '@/components/navigation/page-header'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { EmptyState } from '@/components/ui/empty-state'; +import { KpiCard } from '@/components/dashboard/kpi-card'; +import { VolumeTrend } from '@/components/dashboard/volume-trend'; +import { ResolutionTrend } from '@/components/dashboard/resolution-trend'; +import { QueueHeatmap } from '@/components/dashboard/queue-heatmap'; +import { ActiveEngineers } from '@/components/dashboard/active-engineers'; import { - AlertTriangle, - Database, - Shield, - CalendarClock, RefreshCw, - ArrowRight, - CheckCircle2, - XCircle, - Clock, Activity, Sparkles, - Plug, - KeyRound, + Users, + Layers, + TrendingUp, + Timer, } from 'lucide-react'; -interface IntegrationHealthItem { - key: string; - name: string; - category: string; - status: 'ok' | 'auth_failed' | 'unreachable' | 'not_configured' | 'unknown'; - configured: boolean; - latencyMs?: number; - error?: string | null; - tokenExpiry?: { - envVar: string; - expiresAt: string; - daysRemaining: number; - subject?: string | null; - } | null; - checkedAt: string; -} - -interface IntegrationHealthResponse { - items: IntegrationHealthItem[]; - summary: { - total: number; - ok: number; - failed: number; - notConfigured: number; - expiringWithin14Days: number; - expired: number; - hasIssues: boolean; - }; -} - interface Overview { + today: { + openedToday: number; + resolvedToday: number; + openTotal: number; + slaBreaches: number; + yesterdayOpened: number; + last7DayAvgResolved: number; + }; attention: { linkConflicts: number; itglueUnlinked: number; @@ -78,16 +67,6 @@ interface Overview { fieldGapsCount: number; status: string; }>; - syncHealth: Array<{ - id: string; - name: string; - syncType: string; - isEnabled: boolean; - lastRun: string | null; - lastStatus: string | null; - lastError: string | null; - nextRun: string | null; - }>; stats: { activeCompanies: number; configurationItems: number; @@ -95,7 +74,22 @@ interface Overview { }; } -const STALE_HOURS = 24; +interface Trends { + volumeByDay: Array<{ date: string; count: number }>; + resolutionByDay: Array<{ date: string; avgHours: number | null }>; + queueHeatmap: Array<{ + queueId: number; + queueLabel: string; + total: number; + byPriority: Record; + }>; + activeEngineers: Array<{ + resourceId: string; + name: string; + hours: number; + ticketsTouched: number; + }>; +} function relTime(iso: string | null): string { if (!iso) return 'never'; @@ -110,42 +104,26 @@ function relTime(iso: string | null): string { return `${day} d ago`; } -function isStale(iso: string | null): boolean { - if (!iso) return true; - return Date.now() - new Date(iso).getTime() > STALE_HOURS * 3600_000; -} - -function syncStatusIcon(s: { lastStatus: string | null; lastRun: string | null; isEnabled: boolean }) { - if (!s.isEnabled) return off; - if (s.lastStatus === 'failed') - return ; - if (isStale(s.lastRun)) - return ; - if (s.lastStatus === 'success') - return ; - return ; -} - export default function DashboardPage() { const [data, setData] = useState(null); - const [health, setHealth] = useState(null); + const [trends, setTrends] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); - async function load(): Promise { + async function load() { setLoading(true); try { - const [overviewRes, healthRes] = await Promise.all([ - fetch('/api/dashboard/overview'), - fetch('/api/dashboard/integration-health'), + const [overviewRes, trendsRes] = await Promise.all([ + fetch('/api/dashboard/overview', { cache: 'no-store' }), + fetch('/api/dashboard/trends', { cache: 'no-store' }), ]); if (!overviewRes.ok) { const body = (await overviewRes.json().catch(() => ({}))) as { error?: string }; throw new Error(body.error ?? `HTTP ${overviewRes.status}`); } setData((await overviewRes.json()) as Overview); - if (healthRes.ok) { - setHealth((await healthRes.json()) as IntegrationHealthResponse); + if (trendsRes.ok) { + setTrends((await trendsRes.json()) as Trends); } setError(null); } catch (err) { @@ -159,287 +137,301 @@ export default function DashboardPage() { void load(); }, []); + const today = data?.today; + const openedDelta = today + ? today.openedToday - today.yesterdayOpened + : 0; + const resolvedDelta = today + ? Math.round((today.resolvedToday - today.last7DayAvgResolved) * 10) / 10 + : 0; + return ( -
-
-

Dashboard

- -
+ <> + + + Refresh + + } + /> - {error && ( - - Failed to load - {error} - - )} +
+ {error && ( + + Failed to load + {error} + + )} - {/* NEEDS ATTENTION ----------------------------------------------------- */} -
-

- Needs attention -

-
- 0 ? 'warn' : 'ok'} - /> - - - + {/* TODAY SNAPSHOT ----------------------------------------------- */} +
+

Today

+
+ + + + 0 ? 'attention' : 'default' + } + caption={ + today && today.slaBreaches === 0 + ? 'All on track' + : 'Past due, still open' + } + loading={!data} + /> +
+
+ + {/* NEEDS ATTENTION ---------------------------------------------- */} +
+

Needs attention

+
+ 0 ? 'warn' : 'default' + } + href="/admin/device-link-conflicts" + loading={!data} + /> + + + +
+
+ + {/* QUEUE POSTURE ------------------------------------------------ */} +
+ + + + + Queue posture + + + + {!trends ? ( + + ) : ( + + )} + + + + + + + Active engineers + + + + {!trends ? ( + + ) : ( + + )} + +
-
- {/* RECENT OBSERVATIONS + AUDITS ---------------------------------------- */} -
- - - - - Recent device observations - - - - {data === null && !error ? ( - - ) : data?.observations.length === 0 ? ( -

No observations recorded yet.

- ) : ( -
- {data?.observations.map((o) => ( -
-
-
{o.hostname ?? '(unanchored)'}
-
- {o.kind} - {o.companyName && · {o.companyName}} + {/* TRENDS ------------------------------------------------------- */} +
+ + + + + Volume · last 30 days + + + + {!trends ? ( + + ) : ( + + )} + + + + + + + Mean resolution time · last 30 days + + + + {!trends ? ( + + ) : ( + + )} + + +
+ + {/* RECENT ACTIVITY --------------------------------------------- */} +
+ + + + + Recent device observations + + + + {!data ? ( + + ) : data.observations.length === 0 ? ( + + ) : ( +
+ {data.observations.map((o) => ( +
+
+
{o.hostname ?? '(unanchored)'}
+
+ {o.kind} + {o.companyName && · {o.companyName}} +
+
+
+ {relTime(o.collectedAt)}
-
- {relTime(o.collectedAt)} -
-
- ))} -
- )} - - - - - - - - Recent audits - - - - {data === null && !error ? ( - - ) : data?.audits.length === 0 ? ( -

No endpoint audits yet.

- ) : ( -
- {data?.audits.map((a) => ( -
-
-
{a.hostname ?? '(unanchored)'}
-
- score {a.overallScore?.toFixed(2) ?? '—'} · {a.fieldGapsCount} gaps - {a.companyName && · {a.companyName}} -
-
-
- {relTime(a.generatedAt)} -
-
- ))} -
- )} -
-
-
- - {/* INTEGRATION HEALTH -------------------------------------------------- */} - - - - - Integration health - {health?.summary.hasIssues && ( - issues - )} - - - - {!health ? ( - - ) : ( -
- {health.items - .slice() - .sort((a, b) => statusOrder(a.status) - statusOrder(b.status)) - .map((i) => ( - - ))} -
- )} -
-
- - {/* SYNC HEALTH --------------------------------------------------------- */} - - - Sync health - - - {data === null && !error ? ( - - ) : ( -
- {data?.syncHealth.map((s) => ( -
-
{s.name}
-
- {relTime(s.lastRun)} - {syncStatusIcon(s)} -
+ ))}
- ))} -
- )} -
-
+ )} + + - {/* STATS FOOTER -------------------------------------------------------- */} - {data && ( -

- {data.stats.activeCompanies} companies · {data.stats.configurationItems.toLocaleString()} CIs ·{' '} - {data.stats.xref.total.toLocaleString()} xref rows ( - {data.stats.xref.total > 0 - ? Math.round((data.stats.xref.linked / data.stats.xref.total) * 100) - : 0} - % linked) -

- )} -
- ); -} + + + + + Recent audits + + + + {!data ? ( + + ) : data.audits.length === 0 ? ( + + ) : ( +
+ {data.audits.map((a) => ( +
+
+
{a.hostname ?? '(unanchored)'}
+
+ score {a.overallScore?.toFixed(2) ?? '—'} + {' · '} + {a.fieldGapsCount} gaps + {a.companyName && · {a.companyName}} +
+
+
+ {relTime(a.generatedAt)} +
+
+ ))} +
+ )} +
+
+
-function AttentionCard(props: { - icon: React.ElementType; - value: number | undefined; - label: string; - sub?: string; - href: string; - tone: 'ok' | 'warn' | 'info'; -}) { - const { icon: Icon, value, label, sub, href, tone } = props; - const valueColor = - tone === 'warn' && value && value > 0 - ? 'text-amber-600 dark:text-amber-500' - : tone === 'ok' - ? 'text-foreground' - : 'text-foreground'; - return ( - - - -
- - -
-
- {value === undefined ? '—' : value.toLocaleString()} -
-
{label}
- {sub &&
{sub}
} -
-
- - ); -} - -function statusOrder(s: IntegrationHealthItem['status']): number { - switch (s) { - case 'auth_failed': return 0; - case 'unreachable': return 1; - case 'unknown': return 2; - case 'ok': return 3; - case 'not_configured': return 4; - default: return 5; - } -} - -function statusBadge(item: IntegrationHealthItem) { - const expiringSoon = - item.tokenExpiry && item.tokenExpiry.daysRemaining > 0 && item.tokenExpiry.daysRemaining <= 14; - const expired = item.tokenExpiry && item.tokenExpiry.daysRemaining <= 0; - if (item.status === 'auth_failed' || item.status === 'unreachable') - return ; - if (expired) - return ; - if (expiringSoon) - return ; - if (item.status === 'ok') - return ; - if (item.status === 'unknown') - return ; - return off; -} - -function IntegrationRow({ item }: { item: IntegrationHealthItem }) { - const expired = item.tokenExpiry && item.tokenExpiry.daysRemaining <= 0; - const expiringSoon = - item.tokenExpiry && item.tokenExpiry.daysRemaining > 0 && item.tokenExpiry.daysRemaining <= 14; - const detail = - item.status === 'auth_failed' || item.status === 'unreachable' - ? item.error?.slice(0, 80) - : expired - ? `token expired ${Math.abs(item.tokenExpiry!.daysRemaining).toFixed(0)} d ago` - : expiringSoon - ? `token expires in ${item.tokenExpiry!.daysRemaining.toFixed(0)} d` - : item.latencyMs !== undefined - ? `${item.latencyMs} ms` - : null; - return ( -
-
{item.name}
-
- {detail && {detail}} - {statusBadge(item)} + {/* STATS FOOTER ------------------------------------------------- */} + {data && ( +

+ {data.stats.activeCompanies} active companies ·{' '} + {data.stats.configurationItems.toLocaleString()} configuration items ·{' '} + {data.stats.xref.total.toLocaleString()} xref rows{' '} + ({data.stats.xref.total > 0 + ? Math.round((data.stats.xref.linked / data.stats.xref.total) * 100) + : 0}% linked) +

+ )}
-
+ ); } diff --git a/app/engagement/profile/page.tsx b/app/engagement/profile/page.tsx index db65e1d..e45925a 100644 --- a/app/engagement/profile/page.tsx +++ b/app/engagement/profile/page.tsx @@ -27,6 +27,14 @@ import { } from 'recharts'; import { Users, RefreshCw } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; import { cn } from '@/lib/utils'; interface UserOption { @@ -579,66 +587,61 @@ export default function EngagementProfilePage() { Monthly Breakdown -
- - - - - - - - - - - - - - - - {[...monthly].reverse().map(m => { - const pct = m.hoursWorked > 0 ? Math.round((m.billableHours / m.hoursWorked) * 100) : 0; - const isEmpty = - m.hoursWorked === 0 && m.teamsMessages === 0 && m.emailsSent === 0; - const totalCalls = m.zoomClientCalls + m.teamsCalls; - return ( - - - - - - - - - - - - ); - })} - -
MonthHoursBillableBill %DaysMeetingsMessagesEmailsCalls
{monthLabel(m.month)} - {m.hoursWorked > 0 ? m.hoursWorked.toFixed(1) : '—'} - - {m.billableHours > 0 ? m.billableHours.toFixed(1) : '—'} - - {m.hoursWorked > 0 ? `${pct}%` : '—'} - - {m.daysWorked > 0 ? m.daysWorked : '—'} - - {m.totalMeetings > 0 ? m.totalMeetings : '—'} - - {m.teamsMessages > 0 ? m.teamsMessages : '—'} - - {m.emailsSent > 0 ? m.emailsSent : '—'} - - {totalCalls > 0 ? totalCalls : '—'} -
-
+ + + + Month + Hours + Billable + Bill % + Days + Meetings + Messages + Emails + Calls + + + + {[...monthly].reverse().map(m => { + const pct = m.hoursWorked > 0 ? Math.round((m.billableHours / m.hoursWorked) * 100) : 0; + const isEmpty = + m.hoursWorked === 0 && m.teamsMessages === 0 && m.emailsSent === 0; + const totalCalls = m.zoomClientCalls + m.teamsCalls; + return ( + + {monthLabel(m.month)} + + {m.hoursWorked > 0 ? m.hoursWorked.toFixed(1) : '—'} + + + {m.billableHours > 0 ? m.billableHours.toFixed(1) : '—'} + + + {m.hoursWorked > 0 ? `${pct}%` : '—'} + + + {m.daysWorked > 0 ? m.daysWorked : '—'} + + + {m.totalMeetings > 0 ? m.totalMeetings : '—'} + + + {m.teamsMessages > 0 ? m.teamsMessages : '—'} + + + {m.emailsSent > 0 ? m.emailsSent : '—'} + + + {totalCalls > 0 ? totalCalls : '—'} + + + ); + })} + +
diff --git a/app/globals.css b/app/globals.css index e9d2c6f..7c4dda4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -7,8 +7,8 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --font-sans: var(--font-plex-sans), 'Helvetica Neue', Helvetica, Arial, 'Liberation Sans', sans-serif; + --font-mono: var(--font-plex-mono), ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace; --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -121,3 +121,5 @@ @apply bg-background text-foreground; } } + +@import "./styles/brand.css"; diff --git a/app/layout.tsx b/app/layout.tsx index 760172d..eaf1cd8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,16 +1,32 @@ import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +import { IBM_Plex_Sans, IBM_Plex_Mono } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider"; import { AppNavigation } from "@/components/navigation/app-navigation"; +import { TaglineFooter } from "@/components/branding/tagline-footer"; import { Toaster } from "sonner"; import { AuthProvider } from "@/components/auth/auth-provider"; -const inter = Inter({ subsets: ["latin"] }); +// IBM Plex Sans replaces the brand-mandated Helvetica/Arial. The 2013 +// standards guide called for Helvetica Bold for headers and Helvetica +// Light for the tagline; Plex Sans honors the spirit (clean engineered +// sans) while loading reliably from Google Fonts. Weight 300 covers the +// "Light" usage in the tagline footer. +const plexSans = IBM_Plex_Sans({ + subsets: ["latin"], + weight: ["300", "400", "500", "600", "700"], + variable: "--font-plex-sans", +}); + +const plexMono = IBM_Plex_Mono({ + subsets: ["latin"], + weight: ["400", "500", "600"], + variable: "--font-plex-mono", +}); export const metadata: Metadata = { - title: "Pulse - PSA Management System", - description: "Modern dashboard for Autotask PSA integration with RMM and NMS mapping", + title: "Pulse · Operations console", + description: "Wulf Consulting operations console — tickets, RMM, IT Glue, backups, and analytics in one place.", icons: { icon: [ { url: "/favicon.png", sizes: "any" }, @@ -27,8 +43,8 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - + + -
+
-
{children}
+
{children}
+
diff --git a/app/status/page.tsx b/app/status/page.tsx new file mode 100644 index 0000000..02941c2 --- /dev/null +++ b/app/status/page.tsx @@ -0,0 +1,541 @@ +/* /status — System health dashboard. + * + * Pulls from: + * GET /api/dashboard/integration-health (live API check + token expiry) + * GET /api/dashboard/overview (syncHealth array) + * + * Surfaces what's wrong so the dashboard can stay focused on operational + * KPIs. Polls every 60 s while the page is visible. */ + +'use client'; + +import { useEffect, useState } from 'react'; +import { PageHeader } from '@/components/navigation/page-header'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; +import { Skeleton } from '@/components/ui/skeleton'; +import { EmptyState } from '@/components/ui/empty-state'; +import { StatusLight, type StatusLightState } from '@/components/ui/status-light'; +import { StatusBadge } from '@/components/ui/status-badge'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { WorkerPulse } from '@/components/status/worker-pulse'; +import { + AlertTriangle, + KeyRound, + RefreshCw, + ShieldCheck, + Plug, + Clock, + Activity, +} from 'lucide-react'; + +// ── Types ──────────────────────────────────────────────────────────── + +type IntegrationCategory = + | 'psa' | 'rmm' | 'docs' | 'security' | 'backup' + | 'network' | 'identity' | 'mdm' | 'mail' + | 'finance' | 'productivity' | 'llm'; + +interface IntegrationHealthItem { + key: string; + name: string; + category: IntegrationCategory; + status: 'ok' | 'auth_failed' | 'unreachable' | 'not_configured' | 'unknown' | 'disabled'; + configured: boolean; + latencyMs?: number; + error?: string | null; + tokenExpiry?: { + envVar: string; + expiresAt: string; + daysRemaining: number; + subject?: string | null; + } | null; + checkedAt: string; +} + +interface IntegrationHealthResponse { + items: IntegrationHealthItem[]; + summary: { + total: number; + ok: number; + failed: number; + notConfigured: number; + disabled: number; + expiringWithin14Days: number; + expired: number; + hasIssues: boolean; + }; +} + +interface SyncHealthItem { + id: string; + name: string; + syncType: string; + isEnabled: boolean; + lastRun: string | null; + lastStatus: string | null; + lastError: string | null; + nextRun: string | null; +} + +interface OverviewResponse { + syncHealth: SyncHealthItem[]; +} + +interface WorkerSnapshot { + name: string; + lastActivity: string | null; + inFlight: number; + oneHour: { success: number; failure: number }; +} + +interface WorkersResponse { + workers: WorkerSnapshot[]; +} + +const WORKER_FRESHNESS: Record = { + Analyzer: 5, + 'RMM Overshell': 10, + 'Sync scheduler': 60, +}; + +// ── Helpers ────────────────────────────────────────────────────────── + +const STALE_HOURS = 24; +const POLL_MS = 60_000; + +const CATEGORY_LABELS: Record = { + psa: 'PSA', + rmm: 'RMM', + docs: 'Documentation', + security: 'Security', + backup: 'Backup', + network: 'Network', + identity: 'Identity', + mdm: 'MDM', + mail: 'Mail', + finance: 'Finance', + productivity: 'Productivity', + llm: 'LLM', +}; + +const CATEGORY_ORDER: IntegrationCategory[] = [ + 'psa', 'rmm', 'docs', 'security', 'backup', + 'network', 'identity', 'mdm', 'mail', + 'finance', 'productivity', 'llm', +]; + +function relTime(iso: string | null): string { + if (!iso) return 'never'; + const ms = Date.now() - new Date(iso).getTime(); + if (ms < 0) return 'in the future'; + const min = Math.floor(ms / 60000); + if (min < 1) return 'just now'; + if (min < 60) return `${min} min ago`; + const hr = Math.floor(min / 60); + if (hr < 48) return `${hr} h ago`; + const day = Math.floor(hr / 24); + return `${day} d ago`; +} + +function isStale(iso: string | null): boolean { + if (!iso) return true; + return Date.now() - new Date(iso).getTime() > STALE_HOURS * 3600_000; +} + +function integrationLight(item: IntegrationHealthItem): StatusLightState { + if (item.status === 'disabled') return 'idle'; + const tokenExpired = item.tokenExpiry && item.tokenExpiry.daysRemaining <= 0; + const tokenExpiring = + item.tokenExpiry && + item.tokenExpiry.daysRemaining > 0 && + item.tokenExpiry.daysRemaining <= 14; + if (item.status === 'auth_failed' || item.status === 'unreachable' || tokenExpired) { + return 'error'; + } + if (tokenExpiring) return 'warn'; + if (item.status === 'ok') return 'ok'; + if (item.status === 'not_configured') return 'idle'; + return 'idle'; +} + +function syncLight(item: SyncHealthItem): StatusLightState { + if (!item.isEnabled) return 'idle'; + if (item.lastStatus === 'failed') return 'error'; + if (isStale(item.lastRun)) return 'warn'; + if (item.lastStatus === 'success') return 'ok'; + return 'idle'; +} + +// ── Page ───────────────────────────────────────────────────────────── + +export default function StatusPage() { + const [health, setHealth] = useState(null); + const [overview, setOverview] = useState(null); + const [workers, setWorkers] = useState(null); + const [error, setError] = useState(null); + const [refreshing, setRefreshing] = useState(false); + + async function load(force = false) { + setRefreshing(true); + try { + const [hRes, oRes, wRes] = await Promise.all([ + fetch(`/api/dashboard/integration-health${force ? '?refresh=1' : ''}`, { cache: 'no-store' }), + fetch('/api/dashboard/overview', { cache: 'no-store' }), + fetch('/api/status/workers', { cache: 'no-store' }), + ]); + if (hRes.ok) setHealth((await hRes.json()) as IntegrationHealthResponse); + if (oRes.ok) setOverview((await oRes.json()) as OverviewResponse); + if (wRes.ok) { + const j = (await wRes.json()) as WorkersResponse; + setWorkers(j.workers); + } + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load'); + } finally { + setRefreshing(false); + } + } + + useEffect(() => { + void load(); + const id = setInterval(() => void load(), POLL_MS); + return () => clearInterval(id); + }, []); + + // Roll-up + const overall: StatusLightState = !health + ? 'idle' + : health.summary.failed > 0 || health.summary.expired > 0 + ? 'error' + : health.summary.expiringWithin14Days > 0 || + (overview?.syncHealth.some((s) => syncLight(s) === 'error') ?? false) + ? 'warn' + : 'ok'; + + const overallTitle = !health + ? 'Loading…' + : overall === 'error' + ? `${health.summary.failed} integration${health.summary.failed === 1 ? '' : 's'} failing` + : overall === 'warn' + ? health.summary.expiringWithin14Days > 0 + ? `${health.summary.expiringWithin14Days} token${health.summary.expiringWithin14Days === 1 ? '' : 's'} expiring soon` + : 'Some sync tasks degraded' + : 'All systems operational'; + + // Group integrations + const grouped = (() => { + if (!health) return null; + const map: Record = {}; + for (const item of health.items) { + (map[item.category] ??= []).push(item); + } + return map; + })(); + + const expiring = health?.items + .filter((i) => i.tokenExpiry && i.tokenExpiry.daysRemaining <= 30) + .sort((a, b) => (a.tokenExpiry!.daysRemaining ?? 999) - (b.tokenExpiry!.daysRemaining ?? 999)); + + const failingSyncs = overview?.syncHealth.filter((s) => syncLight(s) === 'error'); + const failingIntegrations = health?.items.filter( + (i) => i.status === 'auth_failed' || i.status === 'unreachable', + ); + + return ( + <> + + + + + } + /> + +
+ {error && ( + + Failed to load status + {error} + + )} + + {/* CONDITIONAL BANNER -------------------------------------------- */} + {(failingIntegrations?.length || failingSyncs?.length) ? ( + + + Action needed + +
    + {failingIntegrations?.map((i) => ( +
  • + {i.name} —{' '} + {i.status === 'auth_failed' ? 'authentication failed' : 'unreachable'} + {i.error && · {i.error.slice(0, 120)}} +
  • + ))} + {failingSyncs?.map((s) => ( +
  • + {s.name} sync failed + {s.lastError && · {s.lastError.slice(0, 120)}} +
  • + ))} +
+
+
+ ) : null} + + {/* INTEGRATION TILES --------------------------------------------- */} +
+
+ +

+ Integrations +

+ {health && ( + + {health.summary.ok} of {health.summary.total - health.summary.notConfigured} healthy + + )} +
+ + {!grouped ? ( +
+ {[1, 2, 3, 4, 5, 6].map((i) => )} +
+ ) : ( +
+ {CATEGORY_ORDER.filter((c) => grouped[c]?.length).map((category) => ( +
+

{CATEGORY_LABELS[category]}

+
+ {grouped[category] + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map((item) => )} +
+
+ ))} +
+ )} +
+ + {/* WORKERS ------------------------------------------------------- */} +
+
+ +

+ Workers +

+
+ {!workers ? ( +
+ {[1, 2, 3].map((i) => )} +
+ ) : ( +
+ {workers.map((w) => ( + + ))} +
+ )} +
+ + {/* TOKEN EXPIRY -------------------------------------------------- */} + {expiring && expiring.length > 0 && ( + + + + + Tokens expiring within 30 days + + + +
+ {expiring.map((item) => { + const days = item.tokenExpiry!.daysRemaining; + const tone = days <= 0 ? 'error' : days <= 14 ? 'warn' : 'pending'; + return ( +
+
+ {item.name} + · {item.tokenExpiry!.envVar} +
+ + {days <= 0 ? `expired ${Math.abs(days)} d ago` : `${days} d`} + +
+ ); + })} +
+
+
+ )} + + {/* SYNC HEALTH --------------------------------------------------- */} + + + + + Scheduled syncs + + + + {!overview ? ( +
+ +
+ ) : overview.syncHealth.length === 0 ? ( +
+ +
+ ) : ( + + + + Schedule + Type + Last run + Next run + Status + + + + + {overview.syncHealth.map((s) => ( + + {s.name} + {s.syncType} + {relTime(s.lastRun)} + {relTime(s.nextRun)} + + {s.isEnabled + ? s.lastStatus === 'failed' + ? failed + : isStale(s.lastRun) + ? stale + : s.lastStatus === 'success' + ? success + : idle + : off} + + + + + + ))} + +
+ )} +
+
+ + {/* COMPLIANCE FOOTER -------------------------------------------- */} + {health && ( +

+ + {health.summary.ok} healthy + · + {health.summary.failed} failing + · + {health.summary.notConfigured} unconfigured + {health.summary.disabled > 0 && ( + <> + · + {health.summary.disabled} disabled + + )} + · + {health.summary.expiringWithin14Days} expiring + · + last checked {relTime(health.items[0]?.checkedAt ?? null)} +

+ )} +
+ + ); +} + +// ── Integration tile ──────────────────────────────────────────────── + +function IntegrationTile({ item }: { item: IntegrationHealthItem }) { + const light = integrationLight(item); + const expired = item.tokenExpiry && item.tokenExpiry.daysRemaining <= 0; + const expiringSoon = + item.tokenExpiry && item.tokenExpiry.daysRemaining > 0 && item.tokenExpiry.daysRemaining <= 14; + + let detail: string | null = null; + if (item.status === 'disabled') detail = 'disabled by operator'; + else if (item.status === 'auth_failed') detail = 'authentication failed'; + else if (item.status === 'unreachable') detail = 'unreachable'; + else if (expired) detail = `token expired ${Math.abs(item.tokenExpiry!.daysRemaining)} d ago`; + else if (expiringSoon) detail = `token expires in ${item.tokenExpiry!.daysRemaining} d`; + else if (item.status === 'not_configured') detail = 'not configured'; + else if (item.status === 'ok' && item.latencyMs !== undefined) detail = `${item.latencyMs} ms`; + else if (item.status === 'unknown' && item.configured) detail = 'configured'; + + return ( +
+ +
+
+

{item.name}

+ {(expired || expiringSoon) && ( + + )} +
+ {detail && ( +

+ {detail} +

+ )} + {item.error && light === 'error' && ( +

+ {item.error.slice(0, 80)} +

+ )} +
+
+ ); +} diff --git a/app/styles/brand.css b/app/styles/brand.css new file mode 100644 index 0000000..4cdc148 --- /dev/null +++ b/app/styles/brand.css @@ -0,0 +1,170 @@ +/* === Wulf Consulting brand layer ==================================== + * + * Authoritative palette per the Logo Standards Guide (docs/StandardsGuide + * (1).pdf, dated 2013-09-04): + * + * Primary blue #0075AD (Pantone 110-7 U) + * Secondary gray #6D6E70 (Pantone 179-10 U) + * Primary face Helvetica / Arial — Bold for headers, Light for tagline + * Tagline "Don't be afraid to cry" (Helvetica Light, gray, 16pt ref.) + * + * Typography update (2026-05): the brand-mandated Helvetica/Arial has + * been replaced with IBM Plex Sans. Plex Sans honors the spirit of the + * mandate (clean engineered sans, supports a Light weight for the + * tagline) while loading reliably from Google Fonts; Helvetica/Arial + * stay in the fallback chain so the look degrades gracefully. Plex + * Mono carries numeric data (KPIs, IDs, timestamps). + * + * Imported once from app/globals.css. The :root overrides below repoint + * --primary / --ring / --accent / fonts; the rest of the shadcn token + * graph stays untouched. + * ==================================================================== */ + +:root { + /* --- Brand-scoped tokens (do not consume directly from components; + they exist so we can reason about brand vs. app intent). ---- */ + --wulf-blue: oklch(0.540 0.136 233.3); /* #0075AD */ + --wulf-blue-700: oklch(0.470 0.142 234.5); /* hover / pressed */ + --wulf-blue-300: oklch(0.760 0.090 232); /* tinted fills */ + --wulf-gray: oklch(0.515 0.001 271); /* #6D6E70 */ + --wulf-gray-100: oklch(0.970 0.002 271); /* near-white surface */ + --wulf-gray-300: oklch(0.880 0.002 271); /* borders */ + --wulf-gray-700: oklch(0.380 0.002 271); /* body text on light bg */ + + /* --- App-level overrides: repoint shadcn tokens to Wulf values. --- + Light theme. --- */ + --primary: var(--wulf-blue); + --primary-foreground: oklch(0.985 0 0); + --accent: var(--wulf-blue); + --accent-foreground: oklch(0.985 0 0); + --ring: var(--wulf-blue); + --sidebar-primary: var(--wulf-blue); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-ring: var(--wulf-blue); + + /* Chart slot 2 = Wulf Blue so single-series charts default to brand. */ + --chart-2: var(--wulf-blue); + + /* Font tokens are wired in app/globals.css (@theme inline) — they + resolve --font-helvetica and --font-plex-mono variables that are + supplied by app/layout.tsx via next/font. We don't override them + here. */ +} + +.dark { + /* Slightly lifted blue for dark surfaces — keeps the tone, raises L. */ + --primary: oklch(0.660 0.150 233.3); + --accent: oklch(0.660 0.150 233.3); + --ring: oklch(0.660 0.150 233.3); + --sidebar-primary: oklch(0.660 0.150 233.3); + --sidebar-ring: oklch(0.660 0.150 233.3); + --chart-2: oklch(0.660 0.150 233.3); +} + +/* === Utility classes ================================================ + * + * These are the canonical helpers for the dashboard refresh. Prefer + * them over re-rolling spacing / typography per page. + * ==================================================================== */ + +@utility num { + /* Numeric data — tabular nums in the mono face. Use on KPI values, + table cells, timestamps, IDs. Never wrap full sentences in this. */ + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + letter-spacing: -0.01em; +} + +@utility num-lg { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + letter-spacing: -0.015em; + font-size: 1.875rem; /* text-3xl */ + line-height: 2.25rem; + font-weight: 500; +} + +@utility num-xl { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + letter-spacing: -0.02em; + font-size: 2.25rem; /* text-4xl */ + line-height: 2.5rem; + font-weight: 600; +} + +@utility metric-label { + /* Pair with .num / .num-lg. 11px uppercase tracked label sitting + above a metric value. */ + font-size: 0.6875rem; /* 11px */ + line-height: 1rem; + letter-spacing: 0.06em; + text-transform: uppercase; + font-weight: 500; + color: var(--muted-foreground); +} + +@utility surface-brand { + background-color: var(--wulf-gray-100); +} + +@utility surface-brand-ink { + background-color: var(--wulf-gray); + color: oklch(0.985 0 0); +} + +@utility rule-brand { + /* The 2px Wulf-blue rule used under PageHeader when accent={true}. + Echoes the standards-guide blue header band. */ + border-bottom: 2px solid var(--wulf-blue); +} + +@utility text-chrome { + /* Sidebar / chrome text. Use for nav text, table column headers, and + anywhere the brand gray should carry weight without going full ink. */ + color: var(--wulf-gray); +} + +@utility border-chrome { + border-color: var(--wulf-gray-300); +} + +@utility tagline { + /* Footer tagline. Helvetica Light, gray, 12px, gentle tracking. + Do NOT use this anywhere except the page footer line. */ + font-family: var(--font-sans); + font-weight: 300; + color: var(--wulf-gray); + font-size: 0.75rem; + letter-spacing: 0.01em; +} + +/* === Wolf-mark watermark ============================================ + * + * Apply .has-mark-watermark to a positioned container; place a child + * with class `mark-watermark` inside. The child stays behind content + * via z-index, anchored to the right edge. + * ==================================================================== */ + +@utility has-mark-watermark { + position: relative; + isolation: isolate; +} + +/* The child class can't be a `@utility` (it's only meaningful in the + parent context), so it's a plain rule. */ +.mark-watermark { + position: absolute; + inset-block: 0; + inset-inline-end: 1.5rem; + z-index: -1; + opacity: 0.04; + pointer-events: none; + height: 100%; + aspect-ratio: 412 / 290; /* W mark intrinsic ratio */ + color: var(--wulf-blue); +} + +.dark .mark-watermark { + opacity: 0.06; +} diff --git a/app/veeam-analysis/page.tsx b/app/veeam-analysis/page.tsx index 034ba30..175760c 100644 --- a/app/veeam-analysis/page.tsx +++ b/app/veeam-analysis/page.tsx @@ -6,6 +6,14 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { Skeleton } from '@/components/ui/skeleton'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell, } from 'recharts'; @@ -144,19 +152,19 @@ function TicketRow({ t, categoryFilter, onFilter }: { return ( <> - setOpen(o => !o)} > - + {open ? : } - - {t.ticket_number} - {t.company_name ?? '—'} - {t.device_hostname ?? '—'} - + + {t.ticket_number} + {t.company_name ?? '—'} + {t.device_hostname ?? '—'} + {catLabel(t.problem_category)} - - {resLabel(t.resolution_type)} - + + {resLabel(t.resolution_type)} + {t.same_day_close - ? + ? : } - - {parseFloat(t.hours_worked).toFixed(2)}h - + + {parseFloat(t.hours_worked).toFixed(2)}h + {t.complexity} - - {timeAgo(t.ticket_created_at)} - + + {timeAgo(t.ticket_created_at)} + {open && ( - - + +
{t.work_summary && ( @@ -222,8 +230,8 @@ function TicketRow({ t, categoryFilter, onFilter }: {
)}
- - +
+
)} ); @@ -668,42 +676,40 @@ export default function VeeamAnalysisPage() { -
- - - - - - - - - - - - - - - - {data.tickets.length > 0 - ? data.tickets.map(t => ( - - )) - : ( - - - - )} - -
- TicketClientDeviceCategoryResolutionSame-dayHoursComplexityAge
- No analyzed tickets yet — run the analysis above. -
-
+ + + + + Ticket + Client + Device + Category + Resolution + Same-day + Hours + Complexity + Age + + + + {data.tickets.length > 0 + ? data.tickets.map(t => ( + + )) + : ( + + + No analyzed tickets yet — run the analysis above. + + + )} + +
{/* Pagination */} {totalPages > 1 && ( diff --git a/app/veeam-comparison/page.tsx b/app/veeam-comparison/page.tsx index 1d80f01..85df37d 100644 --- a/app/veeam-comparison/page.tsx +++ b/app/veeam-comparison/page.tsx @@ -8,6 +8,14 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from '@/components/ui/dialog'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; import { RefreshCw, CheckCircle2, AlertTriangle, WifiOff, GitCompare, Ticket, ChevronRight, ChevronDown, Sparkles, Loader2, @@ -326,21 +334,21 @@ function ClientGroupRow({ group, defaultOpen }: { group: ClientGroup; defaultOpe return ( <> {/* Group summary header — columns align with the detail table below */} - setOpen(o => !o)} > {/* Device col: chevron + org name */} - +
{open ? : } {group.org_name ?? 'Unknown'}
- +
{/* Match col: status pills */} - +
{group.counts.both > 0 && ( {group.counts.both} Both @@ -355,33 +363,33 @@ function ClientGroupRow({ group, defaultOpen }: { group: ClientGroup; defaultOpe {group.counts.offline_suppressed} Offline )}
- +
{/* Pulse shadow col: total devices flagged */} - + {actionable > 0 ? {actionable} device{actionable !== 1 ? 's' : ''} need attention : {group.rows.length} device{group.rows.length !== 1 ? 's' : ''}} - + {/* AT tickets col: ticket count */} - + {group.totalAtTickets > 0 ? <>{group.totalAtTickets} ticket{group.totalAtTickets !== 1 ? 's' : ''}{group.totalAtOpen > 0 && · {group.totalAtOpen} open} : } - - + + {/* Device detail rows */} {open && group.rows.map((row) => { const cfg = STATUS_CONFIG[row.status]; return ( - - + + {row.hostname ?? unknown} - - + + {cfg.label} - - + + {row.pulse ? ( <>
@@ -398,8 +406,8 @@ function ClientGroupRow({ group, defaultOpen }: { group: ClientGroup; defaultOpe ) : ( )} - - + + - - + + ); })} @@ -533,16 +541,16 @@ export default function VeeamComparisonPage() {
- - - - - - - - - - +
DeviceMatchPulse ShadowAutotask Tickets
+ + + Device + Match + Pulse Shadow + Autotask Tickets + + + {groups.length > 0 ? groups.map(group => ( 0} /> )) : ( - - - + + )} - -
+ + {data.matches.length === 0 ? 'No data yet — RPO check must run at least once.' : 'No rows match this filter.'} -
+ +
{groups.length > 0 && (

diff --git a/components/admin/DataTable.tsx b/components/admin/DataTable.tsx index 74e717b..97b094e 100644 --- a/components/admin/DataTable.tsx +++ b/components/admin/DataTable.tsx @@ -1,35 +1,100 @@ +/* DataTable — paginated, sortable, optionally expandable list table. + * + * Backed by @tanstack/react-table v8 in **manual** mode: the parent owns + * data fetching, sort + search dispatch, and the page/pageSize state. + * The table itself just renders what it's given and emits user-input + * callbacks. + * + * External API is intentionally stable from the previous custom + * implementation: + * + * …} // optional + * onSearch={(q) => …} // optional + * onRowClick={(row) => …} // optional + * isLoading={false} // optional + * + * // NEW since the TanStack rewrite: + * getRowCanExpand={(row) => boolean} // optional, default false + * renderSubRow={(row) => <…>} // optional; required when expandable + * /> + * + * The Column shape is the same as before (key/label/sortable/render). + * Internally we translate to ColumnDef so existing consumers keep working + * without code changes. */ + 'use client'; -import { useState } from 'react'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { useMemo, useState } from 'react'; +import { + type ColumnDef, + type ExpandedState, + type Row, + type SortingState, + flexRender, + getCoreRowModel, + getExpandedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Search, ArrowUpDown, ArrowUp, ArrowDown, Loader2 } from 'lucide-react'; -import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; +import { EmptyState } from '@/components/ui/empty-state'; +import { + ArrowDown, + ArrowUp, + ArrowUpDown, + ChevronDown, + ChevronLeft, + ChevronRight, + ChevronsLeft, + ChevronsRight, + Loader2, + Search, +} from 'lucide-react'; +import { cn } from '@/lib/utils'; -interface Column { +export interface Column { key: string; label: string; sortable?: boolean; - render?: (value: any, row: any) => React.ReactNode; + render?: (value: any, row: TData) => React.ReactNode; } -interface DataTableProps { - columns: Column[]; - data: any[]; +export interface DataTableProps { + columns: Column[]; + data: TData[]; totalCount: number; page: number; pageSize: number; onPageChange: (page: number) => void; onSort?: (column: string, direction: 'asc' | 'desc') => void; onSearch?: (query: string) => void; - onRowClick?: (row: any) => void; + onRowClick?: (row: TData) => void; isLoading?: boolean; + /** Per-row gate for expansion. Return true to enable a chevron toggle. */ + getRowCanExpand?: (row: TData) => boolean; + /** Renders the expanded sub-row body when a row is open. */ + renderSubRow?: (row: TData) => React.ReactNode; + /** Empty-state slot. Defaults to a neutral "No results" message. */ + emptyTitle?: string; + emptyDescription?: string; } -export default function DataTable({ +export default function DataTable({ columns, data, totalCount, @@ -40,37 +105,97 @@ export default function DataTable({ onSearch, onRowClick, isLoading = false, -}: DataTableProps) { + getRowCanExpand, + renderSubRow, + emptyTitle = 'No data found', + emptyDescription = 'Try adjusting your search or filters.', +}: DataTableProps) { const [searchQuery, setSearchQuery] = useState(''); - const [sortColumn, setSortColumn] = useState(null); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); + const [sorting, setSorting] = useState([]); + const [expanded, setExpanded] = useState({}); - const totalPages = Math.ceil(totalCount / pageSize); + const expandable = !!renderSubRow; + const totalPages = Math.max(1, Math.ceil(totalCount / pageSize)); - const handleSort = (columnKey: string) => { - if (!onSort) return; - - const newDirection = sortColumn === columnKey && sortDirection === 'asc' ? 'desc' : 'asc'; - setSortColumn(columnKey); - setSortDirection(newDirection); - onSort(columnKey, newDirection); - }; + // Translate the legacy Column shape into TanStack ColumnDef. + const tanstackColumns = useMemo[]>(() => { + const cols: ColumnDef[] = []; + + // Lead expansion column when expansion is enabled. + if (expandable) { + cols.push({ + id: '__expand', + header: () => null, + cell: ({ row }) => + row.getCanExpand() ? ( + + ) : null, + size: 32, + }); + } + + for (const c of columns) { + cols.push({ + id: c.key, + accessorKey: c.key, + enableSorting: !!c.sortable, + header: c.label, + cell: ({ row, getValue }) => + c.render ? c.render(getValue(), row.original) : (getValue() as React.ReactNode), + }); + } + + return cols; + }, [columns, expandable]); + + const table = useReactTable({ + data, + columns: tanstackColumns, + state: { sorting, expanded }, + onSortingChange: (updater) => { + const next = typeof updater === 'function' ? updater(sorting) : updater; + setSorting(next); + // Defer to caller for actual data fetch. + if (onSort && next.length > 0) { + onSort(next[0].id, next[0].desc ? 'desc' : 'asc'); + } + }, + onExpandedChange: setExpanded, + getRowCanExpand: getRowCanExpand + ? (row) => getRowCanExpand(row.original) + : () => expandable, + getCoreRowModel: getCoreRowModel(), + getExpandedRowModel: getExpandedRowModel(), + manualPagination: true, + manualSorting: true, + pageCount: totalPages, + }); const handleSearch = () => { - if (onSearch) { - onSearch(searchQuery); - } + onSearch?.(searchQuery); }; return (

- {/* Search Bar */} {onSearch && (
- + setSearchQuery(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} @@ -84,92 +209,82 @@ export default function DataTable({
)} - {/* Table */} -
+
- - {columns.map((column) => ( - - {column.sortable ? ( - ) : ( - + flexRender(header.column.columnDef.header, header.getContext()) )} - - ) : ( - column.label - )} - - ))} - + + ); + })} + + ))} + {isLoading ? ( - Array.from({ length: 5 }).map((_, index) => ( - - {columns.map((column) => ( - - - - ))} - - )) - ) : data.length === 0 ? ( + renderLoadingRows(table) + ) : table.getRowModel().rows.length === 0 ? ( - -
- -

No data found

-

Try adjusting your search or filters

-
+ +
) : ( - data.map((row, index) => ( - onRowClick?.(row)} - > - {columns.map((column) => ( - - {column.render ? column.render(row[column.key], row) : row[column.key]} - - ))} - + table.getRowModel().rows.map((row) => ( + )) )}
- {/* Pagination */}
-
- Showing {Math.min((page - 1) * pageSize + 1, totalCount)} to{' '} - {Math.min(page * pageSize, totalCount)} of{' '} - {totalCount} results +
+ Showing + {totalCount === 0 ? 0 : Math.min((page - 1) * pageSize + 1, totalCount)} + {' '} + to + {Math.min(page * pageSize, totalCount)} + {' '} + of {totalCount} results
@@ -177,22 +292,22 @@ export default function DataTable({ variant="outline" size="icon" onClick={() => onPageChange(page - 1)} - disabled={page === 1 || isLoading} + disabled={page <= 1 || isLoading} className="h-8 w-8" + aria-label="Previous page" > -
- - Page {page} of {totalPages || 1} - -
+ + Page {page} of {totalPages} + @@ -200,8 +315,9 @@ export default function DataTable({ variant="outline" size="icon" onClick={() => onPageChange(totalPages)} - disabled={page === totalPages || isLoading} + disabled={page >= totalPages || isLoading} className="h-8 w-8" + aria-label="Last page" > @@ -210,3 +326,57 @@ export default function DataTable({
); } + +function ExpandableRow({ + row, + onRowClick, + renderSubRow, + colSpan, +}: { + row: Row; + onRowClick?: (row: TData) => void; + renderSubRow?: (row: TData) => React.ReactNode; + colSpan: number; +}) { + const clickable = !!onRowClick; + return ( + <> + onRowClick?.(row.original)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + {row.getIsExpanded() && renderSubRow && ( + + + {renderSubRow(row.original)} + + + )} + + ); +} + +function SortIcon({ dir }: { dir: 'asc' | 'desc' | null }) { + if (dir === 'asc') return ; + if (dir === 'desc') return ; + return ; +} + +function renderLoadingRows(table: ReturnType>) { + const cols = table.getAllLeafColumns().length; + return Array.from({ length: 5 }).map((_, i) => ( + + {Array.from({ length: cols }).map((_, j) => ( + + + + ))} + + )); +} diff --git a/components/admin/DetailModal.tsx b/components/admin/DetailModal.tsx index 7376784..8bc1f81 100644 --- a/components/admin/DetailModal.tsx +++ b/components/admin/DetailModal.tsx @@ -6,79 +6,23 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { Separator } from '@/components/ui/separator'; import { Calendar, Check, X, Copy, CheckCircle2, Code2, LayoutTemplate, ExternalLink, Phone, Globe, Loader2, User, Building2, MessageSquare, Clock } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { StatusBadge } from '@/components/ui/status-badge'; +import { + priorityBadge, + ticketStatusBadge, + sourceBadge, + classificationBadge, + companyTypeBadge, + publishBadge, + activeBadge, + yesNoBadge, + billableBadge, + approvedBadge, + toneClass, + paletteClass, +} from '@/lib/status-registry'; import { useState, useEffect } from 'react'; -// ── Static picklist maps (Autotask standard values from DB) ────────────────── - -const PRIORITY_MAP: Record = { - 2: { label: 'Critical', cls: 'bg-red-500/15 text-red-600 border border-red-500/30' }, - 3: { label: 'High', cls: 'bg-orange-500/15 text-orange-600 border border-orange-500/30' }, - 4: { label: 'Medium', cls: 'bg-yellow-500/15 text-yellow-700 border border-yellow-500/30' }, - 6: { label: 'Low', cls: 'bg-blue-500/15 text-blue-600 border border-blue-500/30' }, - 7: { label: 'Very Low', cls: 'bg-slate-500/15 text-slate-500 border border-slate-500/30' }, - 8: { label: 'Critical', cls: 'bg-red-500/15 text-red-600 border border-red-500/30' }, - 9: { label: 'High', cls: 'bg-orange-500/15 text-orange-600 border border-orange-500/30' }, - 10: { label: 'Medium', cls: 'bg-yellow-500/15 text-yellow-700 border border-yellow-500/30' }, - 11: { label: 'Low', cls: 'bg-blue-500/15 text-blue-600 border border-blue-500/30' }, -}; - -const STATUS_COLOR: Record = { - 'New': 'bg-blue-500/15 text-blue-600 border border-blue-500/30', - 'In Progress': 'bg-indigo-500/15 text-indigo-600 border border-indigo-500/30', - 'Complete': 'bg-green-500/15 text-green-600 border border-green-500/30', - 'Waiting Customer': 'bg-amber-500/15 text-amber-700 border border-amber-500/30', - 'Waiting Materials': 'bg-orange-500/15 text-orange-600 border border-orange-500/30', - 'Waiting Vendor': 'bg-orange-500/15 text-orange-600 border border-orange-500/30', - 'Waiting Approval': 'bg-purple-500/15 text-purple-600 border border-purple-500/30', - 'On Hold': 'bg-slate-500/15 text-slate-500 border border-slate-500/30', - 'Escalate': 'bg-red-500/15 text-red-600 border border-red-500/30', - 'Escalate to Wulf': 'bg-red-500/15 text-red-600 border border-red-500/30', - 'Escalate to MC': 'bg-red-500/15 text-red-600 border border-red-500/30', - 'Resource Assigned': 'bg-cyan-500/15 text-cyan-600 border border-cyan-500/30', - 'Service Call Scheduled': 'bg-teal-500/15 text-teal-600 border border-teal-500/30', - 'Dispatched': 'bg-teal-500/15 text-teal-600 border border-teal-500/30', - 'Resolved ': 'bg-green-500/15 text-green-600 border border-green-500/30', -}; - -const SOURCE_MAP: Record = { - [-2]: 'System', [-1]: 'Internal', - 1: 'Phone', 2: 'Email', 4: 'Web Portal', 6: 'Monitoring Alert', - 8: 'RMM Alert', 17: 'Chat', 21: 'API', 22: 'Automation', - 27: 'In Person', 29: 'Client Portal', 30: 'Microsoft Teams', - 31: 'Webhook', 33: 'Datto RMM', 34: 'Rewst', 35: 'TimeZest', - 36: 'DeskDirector', 38: 'Huntress', 39: 'Blumira', 40: 'SentinelOne', -}; - -const CLASSIFICATION_MAP: Record = { - 5: { label: 'Block Hour', cls: 'bg-sky-500/15 text-sky-600 border border-sky-500/30' }, - 9: { label: 'Canceled', cls: 'bg-slate-500/15 text-slate-500 border border-slate-500/30' }, - 202: { label: 'Co-Managed', cls: 'bg-cyan-500/15 text-cyan-600 border border-cyan-500/30' }, - 16: { label: 'Gold (Legacy)', cls: 'bg-yellow-500/15 text-yellow-600 border border-yellow-500/30' }, - 206: { label: 'IT Complete w/ Gold Security', cls: 'bg-amber-500/15 text-amber-600 border border-amber-500/30' }, - 207: { label: 'IT Core / Silver Security', cls: 'bg-blue-500/15 text-blue-600 border border-blue-500/30' }, - 203: { label: 'IT Foundation / Bronze Security', cls: 'bg-orange-500/15 text-orange-600 border border-orange-500/30' }, - 205: { label: 'IT Premier / Platinum Security', cls: 'bg-violet-500/15 text-violet-600 border border-violet-500/30' }, - 14: { label: 'Jeopardy Company', cls: 'bg-red-500/15 text-red-600 border border-red-500/30' }, - 201: { label: 'Partner', cls: 'bg-purple-500/15 text-purple-600 border border-purple-500/30' }, - 15: { label: 'Platinum (Legacy)', cls: 'bg-violet-500/15 text-violet-600 border border-violet-500/30' }, - 13: { label: 'Residential (no-pay)', cls: 'bg-slate-500/15 text-slate-500 border border-slate-500/30' }, - 17: { label: 'Silver (Legacy)', cls: 'bg-zinc-500/15 text-zinc-600 border border-zinc-500/30' }, - 12: { label: 'T&M', cls: 'bg-teal-500/15 text-teal-600 border border-teal-500/30' }, - 7: { label: 'Target', cls: 'bg-emerald-500/15 text-emerald-600 border border-emerald-500/30' }, - 200: { label: 'Tools Only', cls: 'bg-slate-500/15 text-slate-500 border border-slate-500/30' }, - 18: { label: 'Bronze (Legacy)', cls: 'bg-orange-500/15 text-orange-600 border border-orange-500/30' }, -}; - -const COMPANY_TYPE_MAP: Record = { - 1: { label: 'Customer', cls: 'bg-green-500/15 text-green-600 border border-green-500/30' }, - 2: { label: 'Lead', cls: 'bg-blue-500/15 text-blue-600 border border-blue-500/30' }, - 3: { label: 'Prospect', cls: 'bg-purple-500/15 text-purple-600 border border-purple-500/30' }, - 4: { label: 'Dead', cls: 'bg-slate-500/15 text-slate-500 border border-slate-500/30' }, - 6: { label: 'Cancelation', cls: 'bg-red-500/15 text-red-600 border border-red-500/30' }, - 7: { label: 'Vendor', cls: 'bg-orange-500/15 text-orange-600 border border-orange-500/30' }, - 8: { label: 'Partner', cls: 'bg-cyan-500/15 text-cyan-600 border border-cyan-500/30' }, -}; - // ── Live lookup types (fetched from DB) ─────────────────────────────────────── interface Lookups { @@ -188,23 +132,24 @@ const COMPANY_GROUPS: FieldGroup[] = [ // ── Helpers ──────────────────────────────────────────────────────────────────── -function ColorBadge({ cls, children }: { cls: string; children: React.ReactNode }) { - return {children}; -} - function resolveLabel(key: string, value: any, type: FieldType | undefined, lookups: Lookups): { display: React.ReactNode; isEmpty: boolean } { if (value === null || value === undefined || value === '') { return { display: , isEmpty: true }; } switch (type) { - case 'bool': + case 'bool': { + const badge = yesNoBadge(Boolean(value)); return { - display: value - ? Yes - : No, + display: ( + + {value ? : } + {badge.label} + + ), isEmpty: false, }; + } case 'date': { try { const d = new Date(value); @@ -221,28 +166,24 @@ function resolveLabel(key: string, value: any, type: FieldType | undefined, look } case 'status': { const label = lookups.statuses[Number(value)] ?? `Status ${value}`; - const cls = STATUS_COLOR[label] ?? 'bg-muted text-muted-foreground border border-border'; - return { display: {label}, isEmpty: false }; + const badge = ticketStatusBadge(label); + return { display: , isEmpty: false }; } case 'priority': { - const p = PRIORITY_MAP[Number(value)]; - return { display: {p?.label ?? `Priority ${value}`}, isEmpty: false }; + return { display: , isEmpty: false }; } case 'source': { - const label = SOURCE_MAP[Number(value)] ?? `Source ${value}`; - return { display: {label}, isEmpty: false }; + return { display: , isEmpty: false }; } case 'queue': { - const qLabel = lookups.queues[Number(value)] ?? `Queue ${value}`; - return { display: {qLabel}, isEmpty: false }; + const label = lookups.queues[Number(value)] ?? `Queue ${value}`; + return { display: {label}, isEmpty: false }; } case 'company_type': { - const ct = COMPANY_TYPE_MAP[Number(value)]; - return { display: {ct?.label ?? `Type ${value}`}, isEmpty: false }; + return { display: , isEmpty: false }; } case 'classification': { - const cl = CLASSIFICATION_MAP[Number(value)]; - return { display: {cl?.label ?? `Classification ${value}`}, isEmpty: false }; + return { display: , isEmpty: false }; } case 'resource': { const name = lookups.resources[Number(value)]; @@ -264,11 +205,11 @@ function resolveLabel(key: string, value: any, type: FieldType | undefined, look } case 'issue_type': { const label = lookups.issueTypes[Number(value)] ?? `Issue ${value}`; - return { display: {label}, isEmpty: false }; + return { display: {label}, isEmpty: false }; } case 'sub_issue_type': { const label = lookups.subIssueTypes[Number(value)] ?? `Sub-Issue ${value}`; - return { display: {label}, isEmpty: false }; + return { display: {label}, isEmpty: false }; } case 'config_item': { const name = lookups.configItems[Number(value)]; @@ -413,8 +354,7 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }:
{(() => { const label = lookups.statuses[Number(data.status)] ?? `Status ${data.status}`; - const cls = STATUS_COLOR[label] ?? 'bg-muted text-muted-foreground border border-border'; - return {label}; + return ; })()}
@@ -427,9 +367,7 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }:
{'is_active' in data && ( - - {data.is_active ? 'Active' : 'Inactive'} - + )}
)} @@ -652,10 +590,10 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }: )} {entry.billable && ( - Billable + )} {entry.approved && ( - Approved + )}
{entry.notes && ( @@ -695,14 +633,6 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }: ) : (
{notes.map((note) => { - const publishCls: Record = { - 1: 'bg-green-500/15 text-green-600 border border-green-500/30', - 2: 'bg-amber-500/15 text-amber-700 border border-amber-500/30', - 4: 'bg-slate-500/15 text-slate-500 border border-slate-500/30', - }; - const publishLabel: Record = { - 1: 'All Users', 2: 'Internal', 4: 'Internal Only', - }; return (
@@ -714,9 +644,7 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }: )} {note.publish != null && ( - - {publishLabel[note.publish] ?? `Publish ${note.publish}`} - + )} {note.title && ( {note.title} diff --git a/components/branding/tagline-footer.tsx b/components/branding/tagline-footer.tsx new file mode 100644 index 0000000..0926a8f --- /dev/null +++ b/components/branding/tagline-footer.tsx @@ -0,0 +1,28 @@ +/* TaglineFooter — bottom-of-page brand line. + * + * "Don't be afraid to cry · Wulf Consulting" in Helvetica Light gray, per + * the standards guide. Mounted once in app/layout.tsx; never inline this + * elsewhere. Hidden on /mobile (which has its own shell). */ + +'use client'; + +import { usePathname } from 'next/navigation'; +import { Separator } from '@/components/ui/separator'; + +export function TaglineFooter() { + const pathname = usePathname(); + if (pathname?.startsWith('/mobile') || pathname?.startsWith('/kiosk')) { + return null; + } + + return ( +
+ +

+ Don't be afraid to cry + + Wulf Consulting +

+
+ ); +} diff --git a/components/branding/wulf-mark.tsx b/components/branding/wulf-mark.tsx new file mode 100644 index 0000000..b56b54a --- /dev/null +++ b/components/branding/wulf-mark.tsx @@ -0,0 +1,44 @@ +/* Wulf Consulting brand mark. + * Wraps the canonical PNG assets shipped from the standards guide: + * public/branding/wulf-mark.png — W mark only (412×290 ratio) + * public/branding/wulf-wordmark.png — full "Wulf Consulting" wordmark + * + * Use `variant="mark"` for the W glyph alone (top-bar brand link, watermark) + * and `variant="wordmark"` for the full lockup (login screen, marketing). */ + +import Image from 'next/image'; +import { cn } from '@/lib/utils'; + +interface WulfMarkProps { + variant?: 'mark' | 'wordmark'; + className?: string; + alt?: string; + priority?: boolean; +} + +export function WulfMark({ + variant = 'mark', + className, + alt, + priority = false, +}: WulfMarkProps) { + const src = + variant === 'wordmark' + ? '/branding/wulf-wordmark.png' + : '/branding/wulf-mark.png'; + const dimensions = + variant === 'wordmark' + ? { width: 686, height: 290 } + : { width: 412, height: 290 }; + + return ( + {alt + ); +} diff --git a/components/dashboard/active-engineers.tsx b/components/dashboard/active-engineers.tsx new file mode 100644 index 0000000..7f5a17a --- /dev/null +++ b/components/dashboard/active-engineers.tsx @@ -0,0 +1,73 @@ +/* ActiveEngineers — top engineers today by hours logged. + * + * Compact list: name + ticket count + hours bar. Sorted by hours + * desc upstream. Empty when no time has been logged yet today. */ + +'use client'; + +import { Activity } from 'lucide-react'; +import { EmptyState } from '@/components/ui/empty-state'; + +interface Engineer { + resourceId: string; + name: string; + hours: number; + ticketsTouched: number; +} + +interface ActiveEngineersProps { + data: Engineer[]; +} + +export function ActiveEngineers({ data }: ActiveEngineersProps) { + if (data.length === 0) { + return ( + + ); + } + + const max = data.reduce((m, e) => Math.max(m, e.hours), 0) || 1; + const totalHours = data.reduce((s, e) => s + e.hours, 0); + const totalTickets = data.reduce((s, e) => s + e.ticketsTouched, 0); + + return ( +
+ {data.map((e) => { + const pct = (e.hours / max) * 100; + return ( +
+
+
{e.name}
+
+
+
+
+
+
{e.hours.toFixed(1)}h
+
+ {e.ticketsTouched}{' '} + ticket{e.ticketsTouched === 1 ? '' : 's'} +
+
+
+ ); + })} +
+ Total today + + {totalHours.toFixed(1)}h{' '} + across {totalTickets}{' '} + ticket{totalTickets === 1 ? '' : 's'} + +
+
+ ); +} diff --git a/components/dashboard/kpi-card.tsx b/components/dashboard/kpi-card.tsx new file mode 100644 index 0000000..2605850 --- /dev/null +++ b/components/dashboard/kpi-card.tsx @@ -0,0 +1,123 @@ +/* KpiCard — primary unit of the operations dashboard. + * + * ┌────────────────────────────────────┐ + * │ LABEL │ + * │ 1,247 ▲ 12 vs yesterday │ + * │ optional caption │ + * └────────────────────────────────────┘ + * + * Numerics use the .num-xl utility (Plex Mono, tabular-nums, large). + * Tone "accent" gets a 2px Wulf-blue left border. + * Tone "warn" gets the destructive border when value > 0. */ + +import Link from 'next/link'; +import { ArrowRight, ArrowUpRight, ArrowDownRight, Minus } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { Card, CardContent } from '@/components/ui/card'; + +export type KpiTone = 'default' | 'accent' | 'warn' | 'attention'; + +interface KpiDelta { + /** Numeric delta (+ for up, - for down). */ + value: number; + /** Suffix shown after the delta. e.g. "vs yesterday". */ + label?: string; + /** When true, "down" is good (e.g., SLA breaches). */ + invertedSentiment?: boolean; +} + +interface KpiCardProps { + label: string; + /** Display value. number formatted with locale, string passed through. null → em-dash. */ + value: number | string | null | undefined; + delta?: KpiDelta; + caption?: React.ReactNode; + tone?: KpiTone; + href?: string; + loading?: boolean; +} + +const TONE_STYLES: Record = { + default: 'border-l-transparent', + accent: 'border-l-primary', + warn: 'border-l-amber-500', + attention: 'border-l-destructive', +}; + +export function KpiCard({ + label, + value, + delta, + caption, + tone = 'default', + href, + loading = false, +}: KpiCardProps) { + const display = + value === null || value === undefined + ? '—' + : typeof value === 'number' + ? value.toLocaleString() + : value; + + const inner = ( + + +
+ {label} + {href && } +
+
+ {loading ? ( + + ) : ( + {display} + )} + {delta && !loading && } +
+ {caption && !loading && ( +
{caption}
+ )} +
+
+ ); + + return href ? ( + + {inner} + + ) : ( + inner + ); +} + +function DeltaIndicator({ delta }: { delta: KpiDelta }) { + const { value, label, invertedSentiment = false } = delta; + if (value === 0) { + return ( + + + {label ?? 'no change'} + + ); + } + const positive = value > 0; + const good = invertedSentiment ? !positive : positive; + const colorClass = good + ? 'text-emerald-600 dark:text-emerald-400' + : 'text-destructive'; + const Icon = positive ? ArrowUpRight : ArrowDownRight; + return ( + + + {Math.abs(value)} + {label && {label}} + + ); +} diff --git a/components/dashboard/queue-heatmap.tsx b/components/dashboard/queue-heatmap.tsx new file mode 100644 index 0000000..b67b8b4 --- /dev/null +++ b/components/dashboard/queue-heatmap.tsx @@ -0,0 +1,134 @@ +/* QueueHeatmap — open tickets by queue × priority. + * + * Geometric grid; each cell is a square tinted with the brand blue. + * Intensity scales linearly to the most-loaded cell (so the heaviest + * cell renders at full saturation). Per-row totals on the right; the + * column headers carry priority labels. + * + * Empty cells render as a thin dashed outline rather than nothing — + * preserves the grid alignment and makes the absence visible. */ + +'use client'; + +import { priorityBadge } from '@/lib/status-registry'; +import { cn } from '@/lib/utils'; + +interface HeatmapRow { + queueId: number; + queueLabel: string; + total: number; + byPriority: Record; +} + +interface QueueHeatmapProps { + data: HeatmapRow[]; + /** Priority IDs in column order. Defaults to Critical→Very Low. */ + priorities?: number[]; +} + +const DEFAULT_PRIORITIES = [2, 3, 4, 6, 7]; + +export function QueueHeatmap({ data, priorities = DEFAULT_PRIORITIES }: QueueHeatmapProps) { + const max = data.reduce( + (m, row) => Math.max(m, ...priorities.map((p) => row.byPriority[p] ?? 0)), + 1, + ); + + if (data.length === 0) { + return ( +

+ No open tickets. +

+ ); + } + + return ( +
+ + + + + {priorities.map((p) => { + const badge = priorityBadge(p); + return ( + + ); + })} + + + + + {data.map((row) => ( + + + {priorities.map((p) => ( + + ))} + + + ))} + +
Queue + {badge.label.slice(0, 3)} + Total
+ {row.queueLabel} + + {row.total} +
+
+ ); +} + +function Cell({ + value, + max, + priorityLabel, + queueLabel, +}: { + value: number; + max: number; + priorityLabel: string; + queueLabel: string; +}) { + if (value === 0) { + return ( + +
+ + ); + } + // Normalize 0..1 then floor into one of 6 opacity steps; smallest cell still + // reads, largest is at 0.85. + const ratio = Math.min(value / max, 1); + const opacity = Math.max(0.18, ratio * 0.85); + return ( + +
0.55 ? 'var(--primary-foreground)' : 'var(--foreground)', + }} + title={`${queueLabel} · ${priorityLabel}: ${value}`} + aria-label={`${queueLabel}, ${priorityLabel}: ${value}`} + > + {value} +
+ + ); +} diff --git a/components/dashboard/resolution-trend.tsx b/components/dashboard/resolution-trend.tsx new file mode 100644 index 0000000..5dc7876 --- /dev/null +++ b/components/dashboard/resolution-trend.tsx @@ -0,0 +1,75 @@ +/* ResolutionTrend — line chart of average resolution hours per day completed, + * last 30 days. Single series, Wulf Blue stroke. */ + +'use client'; + +import { + CartesianGrid, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +interface ResolutionPoint { + date: string; + avgHours: number | null; +} + +interface ResolutionTrendProps { + data: ResolutionPoint[]; + height?: number; +} + +function fmtDate(iso: string) { + return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); +} + +export function ResolutionTrend({ data, height = 180 }: ResolutionTrendProps) { + return ( + + + + + `${v}h`} + /> + fmtDate(String(value))} + formatter={(value) => + value == null ? ['—', 'avg'] : [`${Number(value).toFixed(1)} h`, 'avg'] + } + /> + + + + ); +} diff --git a/components/dashboard/volume-trend.tsx b/components/dashboard/volume-trend.tsx new file mode 100644 index 0000000..5b87a74 --- /dev/null +++ b/components/dashboard/volume-trend.tsx @@ -0,0 +1,78 @@ +/* VolumeTrend — area chart of tickets opened per day for the last 30 days. + * + * Single series, Wulf Blue fill at 20%. No grid, minimal axes — the + * shape is what matters. Tooltip carries the exact count + date. */ + +'use client'; + +import { + Area, + AreaChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +interface VolumePoint { + date: string; + count: number; +} + +interface VolumeTrendProps { + data: VolumePoint[]; + height?: number; +} + +function fmtDate(iso: string) { + return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); +} + +export function VolumeTrend({ data, height = 180 }: VolumeTrendProps) { + return ( + + + + + + + + + + + fmtDate(String(value))} + formatter={(value) => [value ?? 0, 'opened']} + /> + + + + ); +} diff --git a/components/navigation/app-navigation.tsx b/components/navigation/app-navigation.tsx index b50bff9..6f83f78 100644 --- a/components/navigation/app-navigation.tsx +++ b/components/navigation/app-navigation.tsx @@ -29,6 +29,9 @@ import { } from '@/components/ui/navigation-menu'; import { Button } from '@/components/ui/button'; import { ThemeToggle } from '@/components/theme-toggle'; +import { StatusIndicator } from '@/components/navigation/status-indicator'; +import { UserMenu } from '@/components/navigation/user-menu'; +import { MobileNav } from '@/components/navigation/mobile-nav'; import { useSession } from '@/lib/auth-client'; interface NavItem { @@ -132,9 +135,51 @@ const navigationItems: NavItem[] = [ }, { title: 'Admin', - href: '/admin', icon: Activity, - description: 'Sync, mappings, workflow, reports, tools & access' + children: [ + { + title: 'Admin home', + href: '/admin', + icon: Activity, + description: 'Tile index of every admin tool', + }, + { + title: 'Sync schedules', + href: '/admin/sync', + icon: GitCompare, + description: 'Cron-driven syncs for Autotask, IT Glue, Veeam, Engagement, etc.', + }, + { + title: 'Workflow rules', + href: '/admin/workflow', + icon: Brain, + description: 'Classification, AI prompts, ticket digest, and execution history', + }, + { + title: 'RMM Overshell', + href: '/admin/rmm-overshell', + icon: Activity, + description: 'Curated Datto RMM scripts; recent executions and rate limits', + }, + { + title: 'IT Glue write log', + href: '/admin/itglue-writes', + icon: Database, + description: 'Audit-driven changes pushed back to IT Glue; revert from here', + }, + { + title: 'Device-link conflicts', + href: '/admin/device-link-conflicts', + icon: AlertTriangle, + description: 'Cross-system mappings needing a human decision', + }, + { + title: 'Users & roles', + href: '/admin/users', + icon: Users, + description: 'Invite users, manage roles and permissions', + }, + ], }, ]; @@ -163,133 +208,116 @@ export function AppNavigation() { return (
- {/* Logo and App Name */} - - Wulf Consulting -
-

Pulse

-

PSA Management System

-
- + {/* Mobile hamburger + brand */} +
+ + + Wulf Consulting +
+

Pulse

+

Operations console

+
+ +
- {/* Main Navigation — centered */} - + {/* Main Navigation — centered, desktop only */} + - {visibleItems.map((item) => ( - - {item.children ? ( - <> - isActive(child.href)) && "bg-primary text-primary-foreground" - )}> - {item.icon && } - {item.title} - - -
    - {item.children.map((child) => ( -
  • - - -
    - {child.icon && } - {child.title} -
    - {child.description && ( -

    - {child.description} -

    - )} - -
    -
  • - ))} -
-
- - ) : ( - - - {item.icon && } - {item.title} - - - )} -
- ))} + {visibleItems.map((item) => { + const childActive = item.children?.some((c) => isActive(c.href)) ?? false; + const flatActive = isActive(item.href); + // Brand-blue 2px underline marks active state — echoes the + // PageHeader rule rather than filling the button with primary. + const activeRule = 'relative after:absolute after:inset-x-2 after:bottom-0 after:h-[2px] after:bg-primary after:rounded-full'; + return ( + + {item.children ? ( + <> + + {item.icon && } + {item.title} + + +
    + {item.children.map((child) => { + const active = isActive(child.href); + return ( +
  • + + + {child.icon && ( + + )} +
    +
    + {child.title} +
    + {child.description && ( +

    + {child.description} +

    + )} +
    + +
    +
  • + ); + })} +
+
+ + ) : ( + + + {item.icon && } + {item.title} + + + )} +
+ ); + })}
{/* Right Side Actions */} -
+
+ +
); } -// Breadcrumb component for secondary navigation -export interface BreadcrumbItem { - label: string; - href?: string; -} - -interface PageHeaderProps { - title: string; - description?: string; - breadcrumbs?: BreadcrumbItem[]; - actions?: React.ReactNode; -} - -export function PageHeader({ title, description, breadcrumbs, actions }: PageHeaderProps) { - return ( -
-
- {/* Breadcrumbs */} - {breadcrumbs && breadcrumbs.length > 0 && ( - - )} - - {/* Title and Actions */} -
-
-

{title}

- {description && ( -

{description}

- )} -
- {actions &&
{actions}
} -
-
-
- ); -} +// PageHeader moved to ./page-header.tsx; re-exported for backwards compatibility. +export { PageHeader } from '@/components/navigation/page-header'; +export type { BreadcrumbItem, PageHeaderProps } from '@/components/navigation/page-header'; diff --git a/components/navigation/mobile-nav.tsx b/components/navigation/mobile-nav.tsx new file mode 100644 index 0000000..42e2c91 --- /dev/null +++ b/components/navigation/mobile-nav.tsx @@ -0,0 +1,149 @@ +/* MobileNav — hamburger menu for sub-md viewports. + * + * Reuses the same nav config that the desktop NavigationMenu consumes + * (passed in via prop) so the IA stays in sync. Renders as a Sheet + * sliding in from the left, with the same active-route treatment + * (2 px brand-blue underline). */ + +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { Menu } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from '@/components/ui/sheet'; +import { Separator } from '@/components/ui/separator'; + +interface NavItem { + title: string; + href?: string; + icon?: React.ElementType; + description?: string; + children?: NavItem[]; +} + +interface MobileNavProps { + items: NavItem[]; + pathname: string; + isActive: (href?: string) => boolean; +} + +export function MobileNav({ items, pathname, isActive }: MobileNavProps) { + const [open, setOpen] = useState(false); + + // Close the sheet when the path changes — handles the link click before + // useEffect from a route subscription. Effect would also work but this + // is simpler and side-effect-free. + const handleNav = () => setOpen(false); + + return ( + + + + + + + Pulse + + + + + +

+ Don't be afraid to cry · Wulf Consulting +

+
+
+ ); +} + +function MobileNavGroup({ + item, + isActive, + onNav, +}: { + item: NavItem; + isActive: (href?: string) => boolean; + onNav: () => void; +}) { + if (!item.children) { + const active = isActive(item.href); + return ( + + {item.icon && } + {item.title} + + ); + } + + const groupActive = item.children.some((c) => isActive(c.href)); + + return ( +
+

+ {item.icon && } + {item.title} +

+
+ {item.children.map((child) => { + const active = isActive(child.href); + return ( + + {child.icon && } + {child.title} + + ); + })} +
+
+ ); +} diff --git a/components/navigation/page-header.tsx b/components/navigation/page-header.tsx new file mode 100644 index 0000000..ae1469a --- /dev/null +++ b/components/navigation/page-header.tsx @@ -0,0 +1,91 @@ +/* PageHeader — bordered page-title block. + * + * Sits below at the top of any page. Owns the page + * title, description, breadcrumbs, and a right-side actions slot. + * + * Two brand props: + * • accent — replaces the bottom border with a 2px Wulf-blue rule, + * echoing the standards-guide blue header band. + * • watermark — renders the W mark behind the title at very low + * opacity, giving the page a quiet brand signature. + * + * Re-exported from components/navigation/app-navigation.tsx for + * back-compat; new code can import from here directly. */ + +import Link from 'next/link'; +import { cn } from '@/lib/utils'; +import { WulfMark } from '@/components/branding/wulf-mark'; + +export interface BreadcrumbItem { + label: string; + href?: string; +} + +export interface PageHeaderProps { + title: string; + description?: string; + breadcrumbs?: BreadcrumbItem[]; + actions?: React.ReactNode; + /** Replace the standard border-b with a 2px Wulf-blue rule. */ + accent?: boolean; + /** Render the W mark watermark behind the title at ~4% opacity. */ + watermark?: boolean; +} + +export function PageHeader({ + title, + description, + breadcrumbs, + actions, + accent = false, + watermark = false, +}: PageHeaderProps) { + return ( +
+
+ {watermark && ( + + )} + + {breadcrumbs && breadcrumbs.length > 0 && ( + + )} + +
+
+

{title}

+ {description && ( +

{description}

+ )} +
+ {actions && ( +
{actions}
+ )} +
+
+
+ ); +} diff --git a/components/navigation/status-indicator.tsx b/components/navigation/status-indicator.tsx new file mode 100644 index 0000000..41df563 --- /dev/null +++ b/components/navigation/status-indicator.tsx @@ -0,0 +1,71 @@ +/* StatusIndicator — top-bar entry point to /status. + * + * Polls /api/dashboard/integration-health every 60 s, rolls up overall + * state into a single StatusLight, and links to /status. Title attribute + * gives a quick textual hint; click goes to the full page. */ + +'use client'; + +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { StatusLight, type StatusLightState } from '@/components/ui/status-light'; + +interface HealthSummary { + failed: number; + expired: number; + expiringWithin14Days: number; + hasIssues: boolean; +} + +const POLL_MS = 60_000; + +export function StatusIndicator() { + const [summary, setSummary] = useState(null); + + useEffect(() => { + let cancelled = false; + async function load() { + try { + const res = await fetch('/api/dashboard/integration-health', { cache: 'no-store' }); + if (!res.ok || cancelled) return; + const j = (await res.json()) as { summary: HealthSummary }; + if (!cancelled) setSummary(j.summary); + } catch { + /* leave summary null — light renders idle */ + } + } + void load(); + const id = setInterval(() => void load(), POLL_MS); + return () => { + cancelled = true; + clearInterval(id); + }; + }, []); + + const state: StatusLightState = !summary + ? 'idle' + : summary.failed > 0 || summary.expired > 0 + ? 'error' + : summary.expiringWithin14Days > 0 + ? 'warn' + : 'ok'; + + const title = !summary + ? 'System status' + : state === 'error' + ? `${summary.failed + summary.expired} integration issue(s)` + : state === 'warn' + ? `${summary.expiringWithin14Days} token(s) expiring soon` + : 'All systems operational'; + + return ( + + + + ); +} diff --git a/components/navigation/user-menu.tsx b/components/navigation/user-menu.tsx new file mode 100644 index 0000000..508c7f8 --- /dev/null +++ b/components/navigation/user-menu.tsx @@ -0,0 +1,100 @@ +/* UserMenu — top-bar dropdown anchored to the signed-in user. + * + * Shows the user's name, email, and role pill at the top, then offers + * shortcuts to /settings and /settings/security, and a sign-out action + * that bounces back to /auth/sign-in. */ + +'use client'; + +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useSession, signOut } from '@/lib/auth-client'; +import { LogOut, User as UserIcon, ShieldCheck, Settings } from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { StatusBadge } from '@/components/ui/status-badge'; +import { Button } from '@/components/ui/button'; + +export function UserMenu() { + const { data: session } = useSession(); + const router = useRouter(); + + const user = session?.user as + | { name?: string; email?: string; role?: string } + | undefined; + if (!user) return null; + + const initials = (user.name ?? user.email ?? '?') + .split(/[\s@]/) + .filter(Boolean) + .slice(0, 2) + .map((p) => p[0]?.toUpperCase()) + .join(''); + + const role = user.role ?? 'user'; + const roleTone = + role === 'super-admin' ? 'accent' : role === 'admin' ? 'info' : 'neutral'; + const roleLabel = role === 'super-admin' ? 'Super-admin' : role === 'admin' ? 'Admin' : 'User'; + + async function handleSignOut() { + await signOut(); + router.push('/auth/sign-in'); + } + + return ( + + + + + + +
+ {user.name && {user.name}} + {user.email && ( + + {user.email} + + )} + + {roleLabel} + +
+
+ + + + + Settings + + + + + + Security + + + + + + Sign out + +
+
+ ); +} diff --git a/components/status/worker-pulse.tsx b/components/status/worker-pulse.tsx new file mode 100644 index 0000000..b4f5e07 --- /dev/null +++ b/components/status/worker-pulse.tsx @@ -0,0 +1,118 @@ +/* WorkerPulse — single worker heartbeat tile. + * + * State derives from: + * • last activity is fresher than the worker's expected cadence → ok / pending + * • last activity stale → idle (worker quiet, not necessarily broken) + * • any failures in the last hour → warn (degraded) + * • all 1h runs failing → error + * + * Per-worker freshness thresholds: + * • Analyzer — 5 min (poll every 2s, gets work intermittently) + * • RMM Overshell — 10 min + * • Sync scheduler — 60 min (cron-driven; quietest worker) */ + +'use client'; + +import { Card, CardContent } from '@/components/ui/card'; +import { StatusLight, type StatusLightState } from '@/components/ui/status-light'; + +interface WorkerSnapshot { + name: string; + lastActivity: string | null; + inFlight: number; + oneHour: { success: number; failure: number }; +} + +interface WorkerPulseProps { + worker: WorkerSnapshot; + /** Stale threshold in minutes; varies per worker. */ + freshnessMinutes?: number; +} + +function relTime(iso: string | null): string { + if (!iso) return 'never'; + const ms = Date.now() - new Date(iso).getTime(); + if (ms < 0) return 'just now'; + const min = Math.floor(ms / 60000); + if (min < 1) return 'just now'; + if (min < 60) return `${min} min ago`; + const hr = Math.floor(min / 60); + if (hr < 48) return `${hr} h ago`; + const day = Math.floor(hr / 24); + return `${day} d ago`; +} + +export function WorkerPulse({ worker, freshnessMinutes = 30 }: WorkerPulseProps) { + const { lastActivity, inFlight, oneHour } = worker; + const fresh = + lastActivity != null && + Date.now() - new Date(lastActivity).getTime() < freshnessMinutes * 60_000; + + let state: StatusLightState; + let stateLabel: string; + if (oneHour.failure > 0 && oneHour.success === 0) { + state = 'error'; + stateLabel = 'failing'; + } else if (oneHour.failure > 0) { + state = 'warn'; + stateLabel = 'degraded'; + } else if (inFlight > 0) { + state = 'pending'; + stateLabel = 'in flight'; + } else if (fresh) { + state = 'ok'; + stateLabel = 'ok'; + } else { + state = 'idle'; + stateLabel = 'idle'; + } + + return ( + + +
+
+

{worker.name}

+

{stateLabel}

+
+ +
+ +
+ + + 0 ? 'error' : 'default'} /> +
+ +

+ Last activity {relTime(lastActivity)} +

+
+
+ ); +} + +function Stat({ + label, + value, + tone = 'default', +}: { + label: string; + value: number; + tone?: 'default' | 'error'; +}) { + return ( +
+

+ {value} +

+

+ {label} +

+
+ ); +} diff --git a/components/ui/empty-state.tsx b/components/ui/empty-state.tsx new file mode 100644 index 0000000..957f1f2 --- /dev/null +++ b/components/ui/empty-state.tsx @@ -0,0 +1,66 @@ +/* EmptyState — shared empty / zero-data placeholder. + * + * Replaces the "No reports yet." centered text scattered across pages. + * Renders a dashed-border panel with a lucide icon, headline, optional + * description, and an optional CTA button. + * + * Use within Card content, table empty rows, or dialog bodies. */ + +import Link from 'next/link'; +import type { LucideIcon } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; + +interface EmptyStateAction { + label: string; + href?: string; + onClick?: () => void; +} + +interface EmptyStateProps { + icon?: LucideIcon; + title: string; + description?: string; + action?: EmptyStateAction; + size?: 'sm' | 'md'; + className?: string; +} + +export function EmptyState({ + icon: Icon, + title, + description, + action, + size = 'md', + className, +}: EmptyStateProps) { + const padding = size === 'sm' ? 'py-6 px-4' : 'py-10 px-6'; + const iconSize = size === 'sm' ? 'h-5 w-5' : 'h-6 w-6'; + + const button = action ? ( + + ) : null; + + return ( +
+ {Icon && ( + + + + )} +

{title}

+ {description && ( +

{description}

+ )} + {button &&
{button}
} +
+ ); +} diff --git a/components/ui/multi-select.tsx b/components/ui/multi-select.tsx index 215cd2a..0ea1656 100644 --- a/components/ui/multi-select.tsx +++ b/components/ui/multi-select.tsx @@ -98,8 +98,9 @@ export function MultiSelect({ {options.length >= searchThreshold && (
diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx new file mode 100644 index 0000000..cb53bb2 --- /dev/null +++ b/components/ui/sheet.tsx @@ -0,0 +1,143 @@ +"use client" + +import * as React from "react" +import { XIcon } from "lucide-react" +import { Dialog as SheetPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + showCloseButton = true, + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/components/ui/skeleton-helpers.tsx b/components/ui/skeleton-helpers.tsx new file mode 100644 index 0000000..7030da2 --- /dev/null +++ b/components/ui/skeleton-helpers.tsx @@ -0,0 +1,97 @@ +/* Skeleton helpers — standardized loading shells. + * + * Use these instead of one-off `` so the + * loading state matches the post-load layout. Each helper renders a + * shape that approximates a specific final element. + * + * Available helpers: + * • SkeletonRow — single line row (table row, list item) + * • SkeletonRows — repeated rows with a stagger + * • SkeletonCard — KPI / summary card body + * • SkeletonChart — recharts placeholder of fixed height + * • SkeletonHeader — page sub-section title placeholder */ + +import { cn } from '@/lib/utils'; +import { Skeleton } from '@/components/ui/skeleton'; + +interface SkeletonProps { + className?: string; +} + +export function SkeletonRow({ className }: SkeletonProps) { + return ( +
+ + + +
+ ); +} + +export function SkeletonRows({ count = 5, className }: SkeletonProps & { count?: number }) { + return ( +
+ {Array.from({ length: count }).map((_, i) => ( + + ))} +
+ ); +} + +export function SkeletonCard({ className }: SkeletonProps) { + return ( +
+ + + +
+ ); +} + +export function SkeletonChart({ + className, + height = 180, +}: SkeletonProps & { height?: number }) { + return ( +
+
+ +
+
+ ); +} + +export function SkeletonHeader({ className }: SkeletonProps) { + return ( +
+ + +
+ ); +} + +/** SkeletonTable — header row + N body rows. Wrap in Card with p-0 for best fit. */ +export function SkeletonTable({ + rows = 5, + cols = 4, + className, +}: SkeletonProps & { rows?: number; cols?: number }) { + return ( +
+
+ {Array.from({ length: cols }).map((_, i) => ( + + ))} +
+
+ {Array.from({ length: rows }).map((_, i) => ( +
+ {Array.from({ length: cols }).map((_, j) => ( + + ))} +
+ ))} +
+
+ ); +} diff --git a/components/ui/status-badge.tsx b/components/ui/status-badge.tsx new file mode 100644 index 0000000..bdebc52 --- /dev/null +++ b/components/ui/status-badge.tsx @@ -0,0 +1,75 @@ +/* StatusBadge — small rounded pill carrying a status / category label. + * + * Driven by lib/status-registry.ts: callers either pass an explicit + * { variantClass, label } pair (the shape returned by registry helpers + * like priorityBadge / ticketStatusBadge / classificationBadge), or pass + * a `tone` and `children` for one-off semantic states. + * + * Visual: rounded-sm to match the geometric brand direction, no border, + * tinted background carries the weight. */ + +import { cn } from '@/lib/utils'; +import { + TONE_CLASS, + type StatusTone, + type BadgeProps as RegistryBadge, +} from '@/lib/status-registry'; + +interface BaseProps { + size?: 'xs' | 'sm'; + className?: string; + children?: React.ReactNode; +} + +interface ToneVariantProps extends BaseProps { + /** Semantic state when no registry entry exists (children is the label). */ + tone: StatusTone; + variantClass?: never; + label?: never; +} + +interface PassthroughVariantProps extends BaseProps, RegistryBadge { + /** Spread the result of a registry helper directly. */ + tone?: never; +} + +interface CustomVariantProps extends BaseProps { + /** Free-form variant class (e.g. one of PALETTE_CLASS values). */ + variantClass: string; + tone?: never; + label?: string; +} + +type StatusBadgeProps = ToneVariantProps | PassthroughVariantProps | CustomVariantProps; + +const SIZE_CLASS = { + xs: 'text-[10px] leading-4 px-1.5 py-px', + sm: 'text-xs leading-4 px-2 py-0.5', +} as const; + +export function StatusBadge(props: StatusBadgeProps) { + const { size = 'sm', className, children } = props; + + let tint: string; + let body: React.ReactNode = children ?? null; + + if ('tone' in props && props.tone) { + tint = TONE_CLASS[props.tone]; + } else { + tint = props.variantClass; + if (!body && 'label' in props && props.label) body = props.label; + } + + return ( + + {body} + + ); +} diff --git a/components/ui/status-light.tsx b/components/ui/status-light.tsx new file mode 100644 index 0000000..98f9e63 --- /dev/null +++ b/components/ui/status-light.tsx @@ -0,0 +1,53 @@ +/* StatusLight — 8px colored square indicator. + * + * The defining geometry of /status: square, not circle, faintly + * outlined so it reads on white. Five states map to the brand + * status palette. Optional pulse is for "in flight" / "running" rows. */ + +import { cn } from '@/lib/utils'; + +export type StatusLightState = 'ok' | 'warn' | 'error' | 'idle' | 'pending'; + +interface StatusLightProps { + state: StatusLightState; + size?: 'sm' | 'md' | 'lg'; + pulse?: boolean; + label?: string; + className?: string; +} + +const sizeMap = { + sm: 'h-1.5 w-1.5', + md: 'h-2 w-2', + lg: 'h-3 w-3', +} as const; + +const stateMap: Record = { + ok: 'bg-emerald-500', + warn: 'bg-amber-500', + error: 'bg-destructive', + idle: 'bg-muted-foreground/40', + pending: 'bg-primary', +}; + +export function StatusLight({ + state, + size = 'md', + pulse = false, + label, + className, +}: StatusLightProps) { + return ( + + ); +} diff --git a/docs/CR-EmailTagline.png b/docs/CR-EmailTagline.png new file mode 100644 index 0000000..18d6f30 Binary files /dev/null and b/docs/CR-EmailTagline.png differ diff --git a/docs/StandardsGuide (1).pdf b/docs/StandardsGuide (1).pdf new file mode 100644 index 0000000..3ef3197 Binary files /dev/null and b/docs/StandardsGuide (1).pdf differ diff --git a/docs/WULF_RGB (1).png b/docs/WULF_RGB (1).png new file mode 100644 index 0000000..42e8c6a Binary files /dev/null and b/docs/WULF_RGB (1).png differ diff --git a/docs/W_RGB (1).png b/docs/W_RGB (1).png new file mode 100644 index 0000000..370bc75 Binary files /dev/null and b/docs/W_RGB (1).png differ diff --git a/lib/auth-utils.ts b/lib/auth-utils.ts index cf1e3df..73fe59c 100644 --- a/lib/auth-utils.ts +++ b/lib/auth-utils.ts @@ -30,7 +30,7 @@ export async function getSession() { */ export async function requireAuth() { const session = await getSession(); - + if (!session) { return { session: null, @@ -40,7 +40,7 @@ export async function requireAuth() { ), }; } - + return { session, error: null }; } diff --git a/lib/services/email.ts b/lib/services/email.ts index b1d4219..e82a434 100644 --- a/lib/services/email.ts +++ b/lib/services/email.ts @@ -56,7 +56,7 @@ export async function sendMagicLinkEmail({
Pulse
-
PSA Management System
+
Operations console

Sign in to your account

@@ -126,7 +126,7 @@ export async function sendInvitationEmail({
Pulse
-
PSA Management System
+
Operations console

You're invited!

diff --git a/lib/services/integration-health.ts b/lib/services/integration-health.ts index a24769d..1b3720a 100644 --- a/lib/services/integration-health.ts +++ b/lib/services/integration-health.ts @@ -20,7 +20,8 @@ export type HealthStatus = | 'auth_failed' // configured, server returned 401/403 | 'unreachable' // configured, network/DNS/TLS error | 'not_configured' // env vars missing - | 'unknown'; // configured, no live check implemented + | 'unknown' // configured, no live check implemented + | 'disabled'; // operator-suppressed (see INTEGRATIONS_DISABLED env) export interface TokenExpiry { envVar: string; @@ -250,6 +251,54 @@ function checkConfigOnly( }; } +/** + * Operator-side disable list. Set INTEGRATIONS_DISABLED to a comma- or + * space-separated list of integration keys (or aliases) to suppress them + * from the /status page and the top-bar indicator. Disabled entries + * render muted and don't count toward failure summaries. + * + * Aliases: + * sentinelone, s1 → s1 + * datto, datto-rmm → datto_rmm + * itglue, it-glue → itglue + * msgraph, ms-graph → msgraph + */ +const KEY_ALIASES: Record = { + sentinelone: 's1', + 's1': 's1', + datto: 'datto_rmm', + 'datto-rmm': 'datto_rmm', + 'datto_rmm': 'datto_rmm', + itglue: 'itglue', + 'it-glue': 'itglue', + 'it_glue': 'itglue', + msgraph: 'msgraph', + 'ms-graph': 'msgraph', + 'ms_graph': 'msgraph', +}; + +function getDisabledKeys(): Set { + const raw = process.env.INTEGRATIONS_DISABLED; + if (!raw) return new Set(); + return new Set( + raw + .split(/[\s,]+/) + .map((s) => s.trim().toLowerCase()) + .filter(Boolean) + .map((s) => KEY_ALIASES[s] ?? s), + ); +} + +function applyDisableOverlay(items: IntegrationHealth[]): IntegrationHealth[] { + const disabled = getDisabledKeys(); + if (disabled.size === 0) return items; + return items.map((item) => + disabled.has(item.key) + ? { ...item, status: 'disabled', error: null, configured: false } + : item, + ); +} + export async function checkIntegrationHealth(opts?: { skipCache?: boolean }): Promise { if (!opts?.skipCache && cache && cache.expiresAt > Date.now()) { return cache.data; @@ -278,8 +327,9 @@ export async function checkIntegrationHealth(opts?: { skipCache?: boolean }): Pr Promise.resolve(checkConfigOnly('anthropic', 'Anthropic', 'llm', ['ANTHROPIC_API_KEY'])), ]); - cache = { expiresAt: Date.now() + CACHE_TTL_MS, data: results }; - return results; + const overlaid = applyDisableOverlay(results); + cache = { expiresAt: Date.now() + CACHE_TTL_MS, data: overlaid }; + return overlaid; } export function clearIntegrationHealthCache(): void { @@ -291,14 +341,20 @@ export interface HealthSummary { ok: number; failed: number; notConfigured: number; + disabled: number; expiringWithin14Days: number; expired: number; hasIssues: boolean; } export function summarize(items: IntegrationHealth[]): HealthSummary { - let ok = 0, failed = 0, notConfigured = 0, expiringWithin14Days = 0, expired = 0; + let ok = 0, failed = 0, notConfigured = 0, disabled = 0; + let expiringWithin14Days = 0, expired = 0; for (const i of items) { + if (i.status === 'disabled') { + disabled += 1; + continue; + } if (i.status === 'ok' || i.status === 'unknown') ok += 1; else if (i.status === 'auth_failed' || i.status === 'unreachable') failed += 1; else if (i.status === 'not_configured') notConfigured += 1; @@ -309,7 +365,7 @@ export function summarize(items: IntegrationHealth[]): HealthSummary { } return { total: items.length, - ok, failed, notConfigured, + ok, failed, notConfigured, disabled, expiringWithin14Days, expired, hasIssues: failed > 0 || expired > 0 || expiringWithin14Days > 0, }; diff --git a/lib/status-registry.ts b/lib/status-registry.ts new file mode 100644 index 0000000..d7f165d --- /dev/null +++ b/lib/status-registry.ts @@ -0,0 +1,263 @@ +/* Centralized status / priority / classification color registry. + * + * Lifts the hard-coded maps from components/admin/DetailModal.tsx so the + * same labels and colors can be rendered anywhere via . + * + * Two axes: + * • TONE — semantic state (ok / warn / error / info / accent / neutral / pending / inactive) + * • PALETTE — arbitrary categorical hues used when there is no semantic + * intent (e.g. classifications, sources, queue colors) + * + * Components consume `BadgeProps` returned by the helpers below. + * + * The class strings here use Tailwind v4 named-palette colors at /15 + * background and -600/-700 text; the recipe matches the existing app + * style and reads correctly on both light and dark surfaces. */ + +export type StatusTone = + | 'ok' + | 'warn' + | 'error' + | 'info' + | 'accent' + | 'neutral' + | 'pending' + | 'inactive'; + +export type PaletteHue = + | 'red' + | 'orange' + | 'amber' + | 'yellow' + | 'green' + | 'emerald' + | 'sky' + | 'blue' + | 'cyan' + | 'teal' + | 'indigo' + | 'violet' + | 'purple' + | 'slate' + | 'zinc'; + +export interface BadgeProps { + label: string; + /** className snippet for the badge body — pass through to */ + variantClass: string; +} + +/* ── Tone → Tailwind classes ───────────────────────────────────────── */ + +export const TONE_CLASS: Record = { + ok: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400', + warn: 'bg-amber-500/15 text-amber-700 dark:text-amber-400', + error: 'bg-destructive/15 text-destructive', + info: 'bg-primary/15 text-primary', + accent: 'bg-primary/15 text-primary', + neutral: 'bg-slate-500/15 text-slate-700 dark:text-slate-300', + pending: 'bg-sky-500/15 text-sky-700 dark:text-sky-400', + inactive: 'bg-muted text-muted-foreground', +}; + +export const PALETTE_CLASS: Record = { + red: 'bg-red-500/15 text-red-700 dark:text-red-400', + orange: 'bg-orange-500/15 text-orange-700 dark:text-orange-400', + amber: 'bg-amber-500/15 text-amber-700 dark:text-amber-400', + yellow: 'bg-yellow-500/15 text-yellow-700 dark:text-yellow-400', + green: 'bg-green-500/15 text-green-700 dark:text-green-400', + emerald: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400', + sky: 'bg-sky-500/15 text-sky-700 dark:text-sky-400', + blue: 'bg-blue-500/15 text-blue-700 dark:text-blue-400', + cyan: 'bg-cyan-500/15 text-cyan-700 dark:text-cyan-400', + teal: 'bg-teal-500/15 text-teal-700 dark:text-teal-400', + indigo: 'bg-indigo-500/15 text-indigo-700 dark:text-indigo-400', + violet: 'bg-violet-500/15 text-violet-700 dark:text-violet-400', + purple: 'bg-purple-500/15 text-purple-700 dark:text-purple-400', + slate: 'bg-slate-500/15 text-slate-700 dark:text-slate-300', + zinc: 'bg-zinc-500/15 text-zinc-700 dark:text-zinc-300', +}; + +const NEUTRAL_BADGE: BadgeProps = { + label: '—', + variantClass: TONE_CLASS.inactive, +}; + +/* ── Priorities (Autotask numeric IDs) ────────────────────────────── */ + +const PRIORITY_REGISTRY: Record = { + 2: { label: 'Critical', tone: 'error' }, + 3: { label: 'High', tone: 'warn' }, + 4: { label: 'Medium', tone: 'pending' }, + 6: { label: 'Low', tone: 'info' }, + 7: { label: 'Very Low', tone: 'neutral' }, + // Mirrored entries from the older picklist (Autotask renumbered) + 8: { label: 'Critical', tone: 'error' }, + 9: { label: 'High', tone: 'warn' }, + 10: { label: 'Medium', tone: 'pending' }, + 11: { label: 'Low', tone: 'info' }, +}; + +export function priorityBadge(id: number | null | undefined): BadgeProps { + if (id == null) return NEUTRAL_BADGE; + const hit = PRIORITY_REGISTRY[Number(id)]; + if (!hit) return { label: `Priority ${id}`, variantClass: TONE_CLASS.inactive }; + return { label: hit.label, variantClass: TONE_CLASS[hit.tone] }; +} + +/* ── Ticket statuses (string label keyed) ─────────────────────────── */ + +const TICKET_STATUS_REGISTRY: Record = { + 'New': 'info', + 'In Progress': 'pending', + 'Complete': 'ok', + 'Resolved ': 'ok', + 'Waiting Customer': 'warn', + 'Waiting Materials': 'warn', + 'Waiting Vendor': 'warn', + 'Waiting Approval': 'warn', + 'On Hold': 'neutral', + 'Escalate': 'error', + 'Escalate to Wulf': 'error', + 'Escalate to MC': 'error', + 'Resource Assigned': 'pending', + 'Service Call Scheduled': 'pending', + 'Dispatched': 'pending', +}; + +export function ticketStatusBadge(label: string | null | undefined): BadgeProps { + if (!label) return NEUTRAL_BADGE; + const tone = TICKET_STATUS_REGISTRY[label] ?? 'inactive'; + return { label, variantClass: TONE_CLASS[tone] }; +} + +/* ── Sources (Autotask) ───────────────────────────────────────────── */ + +const SOURCE_REGISTRY: Record = { + [-2]: 'System', + [-1]: 'Internal', + 1: 'Phone', + 2: 'Email', + 4: 'Web Portal', + 6: 'Monitoring Alert', + 8: 'RMM Alert', + 17: 'Chat', + 21: 'API', + 22: 'Automation', + 27: 'In Person', + 29: 'Client Portal', + 30: 'Microsoft Teams', + 31: 'Webhook', + 33: 'Datto RMM', + 34: 'Rewst', + 35: 'TimeZest', + 36: 'DeskDirector', + 38: 'Huntress', + 39: 'Blumira', + 40: 'SentinelOne', +}; + +export function sourceBadge(id: number | null | undefined): BadgeProps { + if (id == null) return NEUTRAL_BADGE; + const label = SOURCE_REGISTRY[Number(id)] ?? `Source ${id}`; + return { label, variantClass: PALETTE_CLASS.violet }; +} + +/* ── Classification (Autotask) ────────────────────────────────────── */ + +const CLASSIFICATION_REGISTRY: Record = { + 5: { label: 'Block Hour', hue: 'sky' }, + 9: { label: 'Canceled', hue: 'slate' }, + 202: { label: 'Co-Managed', hue: 'cyan' }, + 16: { label: 'Gold (Legacy)', hue: 'yellow' }, + 206: { label: 'IT Complete w/ Gold Security', hue: 'amber' }, + 207: { label: 'IT Core / Silver Security', hue: 'blue' }, + 203: { label: 'IT Foundation / Bronze Security', hue: 'orange' }, + 205: { label: 'IT Premier / Platinum Security', hue: 'violet' }, + 14: { label: 'Jeopardy Company', hue: 'red' }, + 201: { label: 'Partner', hue: 'purple' }, + 15: { label: 'Platinum (Legacy)', hue: 'violet' }, + 13: { label: 'Residential (no-pay)', hue: 'slate' }, + 17: { label: 'Silver (Legacy)', hue: 'zinc' }, + 12: { label: 'T&M', hue: 'teal' }, + 7: { label: 'Target', hue: 'emerald' }, + 200: { label: 'Tools Only', hue: 'slate' }, + 18: { label: 'Bronze (Legacy)', hue: 'orange' }, +}; + +export function classificationBadge(id: number | null | undefined): BadgeProps { + if (id == null) return NEUTRAL_BADGE; + const hit = CLASSIFICATION_REGISTRY[Number(id)]; + if (!hit) return { label: `Classification ${id}`, variantClass: TONE_CLASS.inactive }; + return { label: hit.label, variantClass: PALETTE_CLASS[hit.hue] }; +} + +/* ── Company type (Autotask) ──────────────────────────────────────── */ + +const COMPANY_TYPE_REGISTRY: Record = { + 1: { label: 'Customer', hue: 'green' }, + 2: { label: 'Lead', hue: 'blue' }, + 3: { label: 'Prospect', hue: 'purple' }, + 4: { label: 'Dead', hue: 'slate' }, + 6: { label: 'Cancelation', hue: 'red' }, + 7: { label: 'Vendor', hue: 'orange' }, + 8: { label: 'Partner', hue: 'cyan' }, +}; + +export function companyTypeBadge(id: number | null | undefined): BadgeProps { + if (id == null) return NEUTRAL_BADGE; + const hit = COMPANY_TYPE_REGISTRY[Number(id)]; + if (!hit) return { label: `Type ${id}`, variantClass: TONE_CLASS.inactive }; + return { label: hit.label, variantClass: PALETTE_CLASS[hit.hue] }; +} + +/* ── Note publish levels (Autotask) ──────────────────────────────── */ + +const PUBLISH_REGISTRY: Record = { + 1: { label: 'All Users', tone: 'ok' }, + 2: { label: 'Internal', tone: 'warn' }, + 4: { label: 'Internal Only', tone: 'neutral' }, +}; + +export function publishBadge(id: number | null | undefined): BadgeProps { + if (id == null) return NEUTRAL_BADGE; + const hit = PUBLISH_REGISTRY[Number(id)]; + if (!hit) return { label: `Publish ${id}`, variantClass: TONE_CLASS.inactive }; + return { label: hit.label, variantClass: TONE_CLASS[hit.tone] }; +} + +/* ── Boolean badges (active / billable / approved / yes / no) ────── */ + +export function activeBadge(active: boolean | null | undefined): BadgeProps { + if (active == null) return NEUTRAL_BADGE; + return active + ? { label: 'Active', variantClass: TONE_CLASS.ok } + : { label: 'Inactive', variantClass: TONE_CLASS.inactive }; +} + +export function yesNoBadge(value: boolean | null | undefined): BadgeProps { + if (value == null) return NEUTRAL_BADGE; + return value + ? { label: 'Yes', variantClass: TONE_CLASS.ok } + : { label: 'No', variantClass: TONE_CLASS.inactive }; +} + +export function billableBadge(value: boolean | null | undefined): BadgeProps { + if (!value) return NEUTRAL_BADGE; + return { label: 'Billable', variantClass: TONE_CLASS.ok }; +} + +export function approvedBadge(value: boolean | null | undefined): BadgeProps { + if (!value) return NEUTRAL_BADGE; + return { label: 'Approved', variantClass: TONE_CLASS.info }; +} + +/* ── Generic helpers (for inline use without registry entry) ─────── */ + +export function toneClass(tone: StatusTone): string { + return TONE_CLASS[tone]; +} + +export function paletteClass(hue: PaletteHue): string { + return PALETTE_CLASS[hue]; +} diff --git a/package-lock.json b/package-lock.json index 7b9db5d..e133403 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "node-cron": "^4.2.1", "nodemailer": "^7.0.12", "pg": "^8.11.0", + "radix-ui": "^1.4.3", "react": "19.2.3", "react-day-picker": "^9.13.0", "react-dom": "19.2.3", @@ -65,6 +66,7 @@ "baseline-browser-mapping": "2.10.8", "eslint": "^9.39.2", "eslint-config-next": "16.1.1", + "shadcn": "^4.6.0", "tailwindcss": "^4.1.18", "tw-animate-css": "^1.4.0", "typescript": "^5", @@ -1628,6 +1630,220 @@ "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", "license": "MIT" }, + "node_modules/@dotenvx/dotenvx": { + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.64.0.tgz", + "integrity": "sha512-6+xRpZaWuHXEqnhBjae+VmQI9Uaqw5Uzu/ScpO+W7ww9Zp3lHSNBoNjFcUxhrCyc7pRGQzyDjhKzloqrPHERiQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "commander": "^11.1.0", + "dotenv": "^17.2.1", + "eciesjs": "^0.4.10", + "execa": "^5.1.1", + "fdir": "^6.2.0", + "ignore": "^5.3.0", + "object-treeify": "1.1.33", + "picomatch": "^4.0.4", + "which": "^4.0.0", + "yocto-spinner": "^1.1.0" + }, + "bin": { + "dotenvx": "src/cli/dotenvx.js" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@dotenvx/dotenvx/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/@dotenvx/dotenvx/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/@dotenvx/dotenvx/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/@dotenvx/dotenvx/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/@dotenvx/dotenvx/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/@dotenvx/dotenvx/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@dotenvx/dotenvx/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/@dotenvx/dotenvx/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/@dotenvx/dotenvx/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@dotenvx/dotenvx/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==", + "dev": true, + "license": "ISC" + }, + "node_modules/@dotenvx/dotenvx/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/@dotenvx/dotenvx/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/yocto-spinner": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.1.0.tgz", + "integrity": "sha512-/BY0AUXnS7IKO354uLLA2eRcWiqDifEbd6unXCsOxkFDAkhgUL3PH9X2bFoaU0YchnDXsF+iKleeTLJGckbXfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18.19" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -1843,6 +2059,19 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@hookform/resolvers": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", @@ -2335,6 +2564,93 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@inquirer/ansi": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", + "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.12.tgz", + "integrity": "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.9.tgz", + "integrity": "sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", + "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz", + "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@ioredis/commands": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", @@ -2386,6 +2702,71 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/@mrleebo/prisma-ast": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.13.1.tgz", @@ -2399,6 +2780,31 @@ "node": ">=16" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.41.8", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.8.tgz", + "integrity": "sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors/node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, "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", @@ -2568,6 +2974,35 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/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/@noble/hashes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", @@ -2628,6 +3063,31 @@ "node": ">=12.4.0" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz", + "integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@oxc-project/types": { "version": "0.127.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", @@ -2723,6 +3183,29 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-accordion": { "version": "1.2.12", "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", @@ -2823,6 +3306,56 @@ } } }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "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-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@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-checkbox": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", @@ -2957,6 +3490,34 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@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-dialog": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", @@ -3122,6 +3683,88 @@ } } }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@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-form/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-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-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-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -3244,6 +3887,38 @@ } } }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-navigation-menu": { "version": "1.2.14", "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", @@ -3280,6 +3955,70 @@ } } }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@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-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "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-popover": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", @@ -3518,6 +4257,38 @@ } } }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@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-roving-focus": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", @@ -3687,6 +4458,39 @@ } } }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@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-slot": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", @@ -3764,6 +4568,198 @@ } } }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@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", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "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-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "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-toolbar/node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "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-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/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-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", @@ -3834,6 +4830,24 @@ } } }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "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", @@ -4315,6 +5329,26 @@ "dev": true, "license": "MIT" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@smithy/abort-controller": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", @@ -5272,6 +6306,87 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@ts-morph/common": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", + "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.3", + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/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/@ts-morph/common/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@ts-morph/common/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/@ts-morph/common/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/@ts-morph/common/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -5482,6 +6597,23 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz", + "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -5494,6 +6626,13 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "node_modules/@types/validate-npm-package-name": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", + "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", @@ -6183,6 +7322,20 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -6206,6 +7359,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -6223,6 +7386,61 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "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", @@ -6438,6 +7656,19 @@ "node": ">=12" } }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "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", @@ -6700,6 +7931,31 @@ "readable-stream": "^3.4.0" } }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/bowser": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", @@ -6803,6 +8059,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/c12": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.2.tgz", @@ -7044,12 +8310,111 @@ "url": "https://polar.sh/cva" } }, + "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-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "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==", + "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/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/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -7068,6 +8433,13 @@ "node": ">=0.10.0" } }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "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", @@ -7129,12 +8501,101 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "dev": true, + "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==", "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "dev": true, + "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/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7296,6 +8757,16 @@ "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -7417,6 +8888,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -7433,6 +8919,16 @@ "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/default-browser": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", @@ -7524,6 +9020,16 @@ "node": ">=0.10" } }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -7567,6 +9073,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -7734,6 +9250,72 @@ "node": ">= 0.4" } }, + "node_modules/eciesjs": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz", + "integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ecies/ciphers": "^0.2.5", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0" + }, + "engines": { + "bun": ">=1", + "deno": ">=2", + "node": ">=16" + } + }, + "node_modules/eciesjs/node_modules/@ecies/ciphers": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz", + "integrity": "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==", + "dev": true, + "license": "MIT", + "engines": { + "bun": ">=1", + "deno": ">=2.7.10", + "node": ">=16" + }, + "peerDependencies": { + "@noble/ciphers": "^1.0.0" + } + }, + "node_modules/eciesjs/node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/eciesjs/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/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==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.241", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.241.tgz", @@ -7747,6 +9329,16 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -7770,6 +9362,26 @@ "node": ">=10.13.0" } }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=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-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -7973,6 +9585,13 @@ "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==", + "dev": true, + "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", @@ -8364,6 +9983,20 @@ "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==", + "dev": true, + "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", @@ -8430,12 +10063,72 @@ "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -8455,6 +10148,69 @@ "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz", + "integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -8518,6 +10274,50 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fast-xml-parser": { "version": "5.2.5", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", @@ -8547,6 +10347,46 @@ "reusify": "^1.0.4" } }, + "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==", + "dev": true, + "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/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -8579,6 +10419,28 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -8633,12 +10495,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -8694,6 +10604,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuzzysort": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", + "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -8713,6 +10630,29 @@ "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==", + "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-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -8747,6 +10687,19 @@ "node": ">=6" } }, + "node_modules/get-own-enumerable-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-own-enumerable-keys/-/get-own-enumerable-keys-1.0.0.tgz", + "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -8761,6 +10714,23 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "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", @@ -8885,6 +10855,16 @@ "dev": true, "license": "MIT" }, + "node_modules/graphql": { + "version": "16.13.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", + "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -9019,6 +10999,24 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/headers-polyfill": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz", + "integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/set-cookie-parser": "^2.4.10", + "set-cookie-parser": "^3.0.1" + } + }, + "node_modules/headers-polyfill/node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "dev": true, + "license": "MIT" + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -9036,6 +11034,16 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hono": { + "version": "4.12.16", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.16.tgz", + "integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -9046,6 +11054,68 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -9179,6 +11249,26 @@ "url": "https://opencollective.com/ioredis" } }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -9221,6 +11311,13 @@ "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==", + "dev": true, + "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", @@ -9412,6 +11509,16 @@ "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -9455,6 +11562,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -9473,6 +11593,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "dev": true, + "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", @@ -9499,6 +11632,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -9526,6 +11666,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-3.0.0.tgz", + "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -9538,6 +11691,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -9557,6 +11717,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-set": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", @@ -9586,6 +11759,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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", @@ -9637,6 +11823,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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", @@ -9786,6 +11985,13 @@ "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-to-ts": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", @@ -9806,6 +12012,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, "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", @@ -9825,6 +12038,19 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "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", @@ -10173,6 +12399,13 @@ "node": ">=10" } }, + "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/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -10214,6 +12447,49 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/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/log-symbols/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -10567,6 +12843,36 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -11154,6 +13460,56 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -11200,6 +13556,75 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msw": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.14.2.tgz", + "integrity": "sha512-D2bTe0tpuf9nw4DA39wFaqUD/hRPKj0DKpo2lAqu+A47Ifg4+h0hbfn6QxVOsiUY2uhgEN6TTpGSHDsc+ysYNg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^6.0.11", + "@mswjs/interceptors": "^0.41.3", + "@open-draft/deferred-promise": "^3.0.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.1.1", + "graphql": "^16.13.2", + "headers-polyfill": "^5.0.1", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.11.7", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.1", + "type-fest": "^5.5.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -11262,6 +13687,16 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next": { "version": "16.1.1", "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz", @@ -11386,6 +13821,46 @@ "node": ">=6.0.0" } }, + "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", + "dev": true, + "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==", + "dev": true, + "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-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", @@ -11407,6 +13882,36 @@ "node": ">=6.0.0" } }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "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/nypm": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", @@ -11459,6 +13964,16 @@ "node": ">= 0.4" } }, + "node_modules/object-treeify": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", + "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/object.assign": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", @@ -11566,6 +14081,19 @@ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "license": "MIT" }, + "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==", + "dev": true, + "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", @@ -11575,6 +14103,22 @@ "wrappy": "1" } }, + "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/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -11611,6 +14155,50 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/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/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -11699,6 +14287,55 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "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/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -11726,6 +14363,13 @@ "dev": true, "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -11846,6 +14490,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-types": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", @@ -11948,6 +14602,19 @@ "node": ">=0.10.0" } }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -11999,6 +14666,22 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prisma": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", @@ -12055,6 +14738,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -12075,6 +14772,22 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "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/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -12096,6 +14809,197 @@ ], "license": "MIT" }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@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-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/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/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/node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "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/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/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -12340,6 +15244,23 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, "node_modules/recharts": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", @@ -12538,6 +15459,26 @@ "url": "https://opencollective.com/unified" } }, + "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/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -12585,6 +15526,30 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "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/rettime": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.8.tgz", + "integrity": "sha512-0fERGXktJTyJ+h8fBEiPxHPEFOu0h15JY7JtwrOVqR5K+vb99ho6IyOo7ekLS3h4sJCzIDy4VWKIbZUfe9njmg==", + "dev": true, + "license": "MIT" + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -12636,6 +15601,34 @@ "integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==", "license": "MIT" }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -12747,6 +15740,13 @@ "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==", + "dev": true, + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -12762,6 +15762,53 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -12817,6 +15864,186 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shadcn": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.6.0.tgz", + "integrity": "sha512-4XeMwFf8ZZxmqQQp+U+Nsq2M+cY4Da8Joo/EaMdHVc4uVuWSTJoeidlZ3gDjyxXCjYB1FLcxYwR4lYQAH8emOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/plugin-transform-typescript": "^7.28.0", + "@babel/preset-typescript": "^7.27.1", + "@dotenvx/dotenvx": "^1.48.4", + "@modelcontextprotocol/sdk": "^1.26.0", + "@types/validate-npm-package-name": "^4.0.2", + "browserslist": "^4.26.2", + "commander": "^14.0.0", + "cosmiconfig": "^9.0.0", + "dedent": "^1.6.0", + "deepmerge": "^4.3.1", + "diff": "^8.0.2", + "execa": "^9.6.0", + "fast-glob": "^3.3.3", + "fs-extra": "^11.3.1", + "fuzzysort": "^3.1.0", + "https-proxy-agent": "^7.0.6", + "kleur": "^4.1.5", + "msw": "^2.10.4", + "node-fetch": "^3.3.2", + "open": "^11.0.0", + "ora": "^8.2.0", + "postcss": "^8.5.6", + "postcss-selector-parser": "^7.1.0", + "prompts": "^2.4.2", + "recast": "^0.23.11", + "stringify-object": "^5.0.0", + "tailwind-merge": "^3.0.1", + "ts-morph": "^26.0.0", + "tsconfig-paths": "^4.2.0", + "validate-npm-package-name": "^7.0.1", + "zod": "^3.24.1", + "zod-to-json-schema": "^3.24.6" + }, + "bin": { + "shadcn": "dist/index.js" + } + }, + "node_modules/shadcn/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/shadcn/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/shadcn/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/shadcn/node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/shadcn/node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shadcn/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shadcn/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/shadcn/node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shadcn/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/sharp": { "version": "0.34.4", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", @@ -12979,6 +16206,19 @@ "devOptional": true, "license": "ISC" }, + "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/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -13040,6 +16280,16 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "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==", + "dev": 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", @@ -13088,6 +16338,16 @@ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", @@ -13095,6 +16355,19 @@ "devOptional": true, "license": "MIT" }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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", @@ -13109,6 +16382,13 @@ "node": ">= 0.4" } }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -13118,6 +16398,31 @@ "safe-buffer": "~5.2.0" } }, + "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/string-width/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/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -13245,6 +16550,40 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stringify-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz", + "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-keys": "^1.0.0", + "is-obj": "^3.0.0", + "is-regexp": "^3.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/yeoman/stringify-object?sponsor=1" + } + }, + "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-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -13255,6 +16594,19 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -13348,6 +16700,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tailwind-merge": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", @@ -13486,6 +16851,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.30" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -13499,6 +16884,29 @@ "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -13538,6 +16946,17 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-morph": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", + "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.27.0", + "code-block-writer": "^13.0.3" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -13605,6 +17024,37 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", + "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -13746,6 +17196,19 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -13833,6 +17296,26 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "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", @@ -13868,6 +17351,16 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -13966,6 +17459,26 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -14471,6 +17984,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -14603,6 +18126,69 @@ "node": ">=0.10.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==", + "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/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -14633,12 +18219,96 @@ "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==", + "dev": true, + "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==", "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==", + "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/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" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -14688,6 +18358,16 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, "node_modules/zod-validation-error": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", diff --git a/package.json b/package.json index 24f822a..71d3fcb 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "node-cron": "^4.2.1", "nodemailer": "^7.0.12", "pg": "^8.11.0", + "radix-ui": "^1.4.3", "react": "19.2.3", "react-day-picker": "^9.13.0", "react-dom": "19.2.3", @@ -68,6 +69,7 @@ "baseline-browser-mapping": "2.10.8", "eslint": "^9.39.2", "eslint-config-next": "16.1.1", + "shadcn": "^4.6.0", "tailwindcss": "^4.1.18", "tw-animate-css": "^1.4.0", "typescript": "^5", diff --git a/public/branding/cr-email-tagline.png b/public/branding/cr-email-tagline.png new file mode 100644 index 0000000..18d6f30 Binary files /dev/null and b/public/branding/cr-email-tagline.png differ diff --git a/public/branding/wulf-mark.png b/public/branding/wulf-mark.png new file mode 100644 index 0000000..370bc75 Binary files /dev/null and b/public/branding/wulf-mark.png differ diff --git a/public/branding/wulf-wordmark.png b/public/branding/wulf-wordmark.png new file mode 100644 index 0000000..42e8c6a Binary files /dev/null and b/public/branding/wulf-wordmark.png differ