diff --git a/README.md b/README.md index 92e2fb9..0eeb526 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,14 @@ cd bms ### 2. Create the environment files +Copy the example files and fill in your values: + ```bash cp backend/.env.example backend/.env cp frontend/.env.local.example frontend/.env.local ``` -The defaults work as-is — they already match the service names in `docker-compose.yml`. **The app runs in demo mode with authentication disabled**, so there are no keys to obtain before the first run. See [Environment Variables](#environment-variables) below. +Open each file and follow the inline comments. At minimum you need to set the Clerk keys (see [Environment Variables](#environment-variables) below). ### 3. Start all services @@ -70,14 +72,6 @@ Open your browser at **http://your-server:5646** ## Environment Variables -> **Authentication is disabled.** The app runs in demo mode: `frontend/proxy.ts` passes every -> request through and there is no auth provider in `app/layout.tsx`. Deploy it on a trusted -> network (LAN, VPN, or behind an authenticating reverse proxy) — every route is open. -> -> To turn auth back on: reinstall `@clerk/nextjs`, wrap the tree in `` in -> `app/layout.tsx`, restore `` in `components/layout/topbar.tsx`, re-add the -> `sign-in` / `sign-up` route groups, and swap `proxy.ts` for `clerkMiddleware`. - ### `backend/.env` ```env @@ -88,6 +82,11 @@ DATABASE_URL=postgresql+asyncpg://dcim:dcim_pass@db:5432/dcim MQTT_HOST=mqtt MQTT_PORT=1883 +# Clerk authentication +# Get these from https://dashboard.clerk.com → Your App → API Keys +CLERK_SECRET_KEY=sk_test_REPLACE_ME +CLERK_JWKS_URL=https://YOUR_APP.clerk.accounts.dev/.well-known/jwks.json + # CORS — add your frontend origin if you expose the backend directly # Leave empty when using the built-in Next.js proxy (recommended) CORS_ORIGINS=[] @@ -98,14 +97,24 @@ DEBUG=true ### `frontend/.env.local` ```env +# Clerk authentication (same app as above) +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_REPLACE_ME +CLERK_SECRET_KEY=sk_test_REPLACE_ME + +# Clerk redirect paths — no need to change these +NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in +NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up +NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard +NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard + # API path — leave as-is, Next.js proxies /api/backend/* to the backend internally NEXT_PUBLIC_API_URL=/api/backend - -# Backend internal URL (used by the Next.js server-side proxy, not sent to the browser) -# In Docker: http://backend:8000 In local dev: http://localhost:8000 -BACKEND_INTERNAL_URL=http://backend:8000 ``` +> **Where do I get Clerk keys?** +> Sign up free at https://clerk.com → create an application → go to **API Keys**. +> Copy the **Publishable key** and **Secret key** into both files above. + --- ## Verify Everything Is Running @@ -172,26 +181,6 @@ Browser → Reverse Proxy → :5646 (Next.js) --- -## Deploying to the Unraid Server - -The live instance runs on the Unraid Docker host at `192.168.1.249` as the Portainer stack -**`bms`** — reachable at **http://192.168.1.249:5646/dashboard**. - -```bash -python3 ops/deploy.py # build all four images + deploy/update the stack -python3 ops/deploy.py --no-build # redeploy the stack using the existing images -``` - -There is no `docker` CLI in WSL and no SSH to the Unraid box, so `ops/deploy.py` ships each -service's build context to the remote daemon's `/build` endpoint over the Portainer API -(token at `~/.portainer-token`), then pushes `docker-compose.prod.yml` as a Portainer stack. -The script is idempotent — rerun it after any code change. - -`docker-compose.prod.yml` differs from the dev `docker-compose.yml`: images instead of build -contexts, no source bind mounts, no `--reload`, and only the frontend publishes a host port. -Postgres data is bind-mounted to `/mnt/user/appdata/bms/db` so it lands on the Unraid array -instead of growing inside `docker.img`, and it survives redeploys. - ## Local Development (without Docker) Useful if you want hot-reload on the frontend or backend without rebuilding containers. diff --git a/backend/.env.example b/backend/.env.example index 11aecd0..3b7934b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -11,6 +11,11 @@ DATABASE_URL=postgresql+asyncpg://dcim:dcim_pass@db:5432/dcim MQTT_HOST=mqtt MQTT_PORT=1883 +# Clerk authentication +# Get these from https://dashboard.clerk.com → Your App → API Keys +CLERK_SECRET_KEY=sk_test_REPLACE_ME +CLERK_JWKS_URL=https://YOUR_APP.clerk.accounts.dev/.well-known/jwks.json + # CORS origins — only needed if the backend is accessed directly from a browser # Example: CORS_ORIGINS=["https://yourdomain.com"] # Leave empty when using the built-in Next.js proxy (recommended) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index 2429a30..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,116 +0,0 @@ -# Production stack — deployed to the Unraid Docker host as a Portainer stack. -# -# Differences from docker-compose.yml (the local dev file): -# * No build: sections. ops/deploy.py builds the four images on the Unraid daemon -# first; this file only references them by tag. -# * No source bind mounts and no uvicorn --reload. Code lives in the image. -# * No env_file. Everything is set inline — there are no .env files on the host. -# * Only the frontend publishes a host port. Postgres and MQTT stay on the -# internal bms_net network, as the README's reverse-proxy note recommends. -# * Postgres data is bind-mounted to /mnt/user/appdata/bms/db so it lands on the -# Unraid array rather than growing inside docker.img. -# -# Deploy / redeploy with: python3 ops/deploy.py - -services: - - # ── MQTT Broker ────────────────────────────────────────────────── - mqtt: - image: bms-mqtt:latest - container_name: bms_mqtt - restart: unless-stopped - networks: [bms_net] - healthcheck: - test: ["CMD-SHELL", "mosquitto_sub -t '$$SYS/#' -C 1 -i healthcheck -W 3"] - interval: 10s - timeout: 5s - retries: 5 - - # ── PostgreSQL + TimescaleDB ───────────────────────────────────── - db: - image: timescale/timescaledb:latest-pg16 - container_name: bms_db - restart: unless-stopped - networks: [bms_net] - environment: - POSTGRES_USER: dcim - POSTGRES_PASSWORD: dcim_pass - POSTGRES_DB: dcim - volumes: - - /mnt/user/appdata/bms/db:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U dcim -d dcim"] - interval: 10s - timeout: 5s - retries: 5 - - # ── FastAPI backend ────────────────────────────────────────────── - backend: - image: bms-backend:latest - container_name: bms_backend - restart: unless-stopped - networks: [bms_net] - environment: - DATABASE_URL: postgresql+asyncpg://dcim:dcim_pass@db:5432/dcim - MQTT_HOST: mqtt - MQTT_PORT: "1883" - CORS_ORIGINS: "[]" - DEBUG: "false" - depends_on: - db: - condition: service_healthy - mqtt: - condition: service_healthy - healthcheck: - test: ["CMD-SHELL", "curl -sf http://localhost:8000/api/health || exit 1"] - interval: 10s - timeout: 5s - retries: 10 - start_period: 20s - - # ── Simulator bots (seed first, then run bots) ─────────────────── - simulators: - image: bms-simulators:latest - container_name: bms_simulators - restart: unless-stopped - networks: [bms_net] - environment: - MQTT_HOST: mqtt - MQTT_PORT: "1883" - DATABASE_URL: postgresql://dcim:dcim_pass@db:5432/dcim - SEED_MINUTES: "30" - depends_on: - db: - condition: service_healthy - mqtt: - condition: service_healthy - backend: - condition: service_healthy - - # ── Next.js frontend (the only publicly reachable service) ─────── - frontend: - image: bms-frontend:latest - container_name: bms_frontend - restart: unless-stopped - networks: [bms_net] - ports: - - "5646:5646" - environment: - PORT: "5646" - HOSTNAME: "0.0.0.0" - NODE_ENV: production - BACKEND_INTERNAL_URL: http://backend:8000 - depends_on: - - backend - -networks: - # The Unraid daemon's default address pools are fully subnetted — 24 existing - # stacks have claimed every 172.17-172.31/16 and 192.168.x/20 slot, so letting - # Docker auto-allocate fails with "all predefined address pools have been fully - # subnetted". An explicit subnet bypasses the allocator entirely. - # 172.31.42.0/24 is free: the only other 172.31 tenant is wg0 on 172.31.200.0/24. - bms_net: - driver: bridge - ipam: - config: - - subnet: 172.31.42.0/24 diff --git a/frontend/.env.local.example b/frontend/.env.local.example index 6b9ea96..84cfe6e 100644 --- a/frontend/.env.local.example +++ b/frontend/.env.local.example @@ -1,11 +1,19 @@ # ───────────────────────────────────────────────────────────────── # BMS Frontend — Environment Variables -# Copy this file to .env.local. The defaults below work as-is. +# Copy this file to .env.local and fill in your values. # Never commit .env.local to git. # ───────────────────────────────────────────────────────────────── -# -# Authentication is disabled (demo mode) — see proxy.ts and the README. -# There are no auth keys to set. + +# Clerk authentication +# Get these from https://dashboard.clerk.com → Your App → API Keys +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_REPLACE_ME +CLERK_SECRET_KEY=sk_test_REPLACE_ME + +# Clerk redirect paths — no change needed +NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in +NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up +NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard +NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard # API path — leave as-is # All /api/backend/* requests are proxied server-side to the FastAPI backend diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 41d797d..69c252c 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -4,9 +4,7 @@ FROM node:22-alpine AS base FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app -# pnpm-workspace.yaml carries ignoredBuiltDependencies — without it pnpm 10+ -# aborts the install with ERR_PNPM_IGNORED_BUILDS. -COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* ./ +COPY package.json pnpm-lock.yaml* ./ RUN corepack enable pnpm && pnpm install --frozen-lockfile # Build the app diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 31864ac..3310bf0 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { ClerkProvider } from "@clerk/nextjs"; import { ThemeProvider } from "@/components/theme-provider"; import { TooltipProvider } from "@/components/ui/tooltip"; import "./globals.css"; @@ -24,22 +25,22 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - // Demo mode: no auth provider — see proxy.ts. Re-add here - // (and the keys in .env.local) if authentication is ever turned back on. return ( - - - - - {children} - - - - + + + + + + {children} + + + + + ); } diff --git a/frontend/app/sign-in/[[...sign-in]]/page.tsx b/frontend/app/sign-in/[[...sign-in]]/page.tsx new file mode 100644 index 0000000..cdd17be --- /dev/null +++ b/frontend/app/sign-in/[[...sign-in]]/page.tsx @@ -0,0 +1,21 @@ +import { SignIn } from "@clerk/nextjs"; +import { Database } from "lucide-react"; + +export default function SignInPage() { + return ( +
+
+
+
+ +
+
+

DemoBMS

+

Infrastructure Management

+
+
+ +
+
+ ); +} diff --git a/frontend/app/sign-up/[[...sign-up]]/page.tsx b/frontend/app/sign-up/[[...sign-up]]/page.tsx new file mode 100644 index 0000000..9eefc57 --- /dev/null +++ b/frontend/app/sign-up/[[...sign-up]]/page.tsx @@ -0,0 +1,21 @@ +import { SignUp } from "@clerk/nextjs"; +import { Database } from "lucide-react"; + +export default function SignUpPage() { + return ( +
+
+
+
+ +
+
+

DemoBMS

+

Infrastructure Management

+
+
+ +
+
+ ); +} diff --git a/frontend/components/layout/topbar.tsx b/frontend/components/layout/topbar.tsx index 2c3f982..f2c1bfb 100644 --- a/frontend/components/layout/topbar.tsx +++ b/frontend/components/layout/topbar.tsx @@ -4,6 +4,7 @@ import { Bell, Menu, Moon, Sun } from "lucide-react"; import { SimulatorPanel } from "@/components/simulator/SimulatorPanel"; import { usePathname } from "next/navigation"; import Link from "next/link"; +import { UserButton } from "@clerk/nextjs"; import { useTheme } from "next-themes"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -84,6 +85,15 @@ export function Topbar() { > {theme === "dark" ? : } + + {/* Clerk user button */} + ); diff --git a/frontend/package.json b/frontend/package.json index 4c273a3..c4f7589 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,7 +2,6 @@ "name": "frontend", "version": "0.1.0", "private": true, - "packageManager": "pnpm@11.20.0", "scripts": { "dev": "next dev", "build": "next build", @@ -10,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@clerk/nextjs": "^7.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.577.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 707518b..3be2904 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@clerk/nextjs': + specifier: ^7.0.1 + version: 7.0.1(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -19,7 +22,7 @@ importers: version: 0.577.0(react@19.2.3) next: specifier: 16.1.6 - version: 16.1.6(@babel/core@7.29.0(supports-color@7.2.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -59,13 +62,13 @@ importers: version: 19.2.3(@types/react@19.2.14) eslint: specifier: ^9 - version: 9.39.3(jiti@2.6.1)(supports-color@7.2.0) + version: 9.39.3(jiti@2.6.1) eslint-config-next: specifier: 16.1.6 - version: 16.1.6(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + version: 16.1.6(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) shadcn: specifier: ^3.8.5 - version: 3.8.5(@types/node@20.19.35)(supports-color@7.2.0)(typescript@5.9.3) + version: 3.8.5(@types/node@20.19.35)(typescript@5.9.3) tailwindcss: specifier: ^4 version: 4.2.1 @@ -215,6 +218,37 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@clerk/backend@3.0.1': + resolution: {integrity: sha512-U5E+KpNlFlaJRdpOoOltIV8o1YjTWw5affsugEb/kIvGceSP6SzvNEBr/TnCY+2yu87Vq22ShEycpQBlse2Bkw==} + engines: {node: '>=20.9.0'} + + '@clerk/nextjs@7.0.1': + resolution: {integrity: sha512-sm5vpQkVUpTWFGe6k4wELL4zTve4JK5IWd8epBj6x4fBAWJMqtC3QeWrvaI7Sl8Ef/0STOM41FcwsVtVMk4v9Q==} + engines: {node: '>=20.9.0'} + peerDependencies: + next: ^15.2.8 || ^15.3.8 || ^15.4.10 || ^15.5.9 || ^15.6.0-0 || ^16.0.10 || ^16.1.0-0 + react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + + '@clerk/react@6.0.1': + resolution: {integrity: sha512-zq6vfH7Yiul4PPxUmyl/iW6CZbIzxfHvhXLxZK9zbqHQEy9xDlIQirhWIiHucBqMWTJruudY5KCV5WBe2xmfww==} + engines: {node: '>=20.9.0'} + peerDependencies: + react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + + '@clerk/shared@4.0.0': + resolution: {integrity: sha512-Z3QhVud7FM9SBgSGxyUdC+nDg6vro+5zJ5gDO1To3FDzRLWKW4xIGd5y8UBqWZMMMHWaSDiZvYlUynb+gs8PnQ==} + engines: {node: '>=20.9.0'} + peerDependencies: + react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + '@dotenvx/dotenvx@1.52.0': resolution: {integrity: sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w==} hasBin: true @@ -1336,6 +1370,9 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1437,6 +1474,9 @@ packages: '@tailwindcss/postcss@4.2.1': resolution: {integrity: sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==} + '@tanstack/query-core@5.90.16': + resolution: {integrity: sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==} + '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} @@ -2059,6 +2099,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2337,6 +2381,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -2474,6 +2521,9 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -2785,6 +2835,10 @@ packages: jose@6.2.0: resolution: {integrity: sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==} + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3471,6 +3525,9 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3545,10 +3602,16 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -3931,20 +3994,20 @@ snapshots: '@babel/compat-data@7.29.0': {} - '@babel/core@7.29.0(supports-color@7.2.0)': + '@babel/core@7.29.0': dependencies: '@babel/code-frame': 7.29.0 '@babel/generator': 7.29.1 '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helpers': 7.28.6 '@babel/parser': 7.29.0 '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0(supports-color@7.2.0) + '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -3971,41 +4034,41 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.28.5(supports-color@7.2.0) + '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@7.2.0) - '@babel/traverse': 7.29.0(supports-color@7.2.0) + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 semver: 6.3.1 transitivePeerDependencies: - supports-color '@babel/helper-globals@7.28.0': {} - '@babel/helper-member-expression-to-functions@7.28.5(supports-color@7.2.0)': + '@babel/helper-member-expression-to-functions@7.28.5': dependencies: - '@babel/traverse': 7.29.0(supports-color@7.2.0) + '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.28.6(supports-color@7.2.0)': + '@babel/helper-module-imports@7.28.6': dependencies: - '@babel/traverse': 7.29.0(supports-color@7.2.0) + '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/helper-module-imports': 7.28.6(supports-color@7.2.0) + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0(supports-color@7.2.0) + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -4015,18 +4078,18 @@ snapshots: '@babel/helper-plugin-utils@7.28.6': {} - '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/helper-member-expression-to-functions': 7.28.5(supports-color@7.2.0) + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.29.0(supports-color@7.2.0) + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.27.1(supports-color@7.2.0)': + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.29.0(supports-color@7.2.0) + '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -4046,43 +4109,43 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0(supports-color@7.2.0))': + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0(supports-color@7.2.0))': + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@7.2.0) - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0(supports-color@7.2.0)) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.28.5(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0(supports-color@7.2.0)) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) transitivePeerDependencies: - supports-color @@ -4092,7 +4155,7 @@ snapshots: '@babel/parser': 7.29.0 '@babel/types': 7.29.0 - '@babel/traverse@7.29.0(supports-color@7.2.0)': + '@babel/traverse@7.29.0': dependencies: '@babel/code-frame': 7.29.0 '@babel/generator': 7.29.1 @@ -4100,7 +4163,7 @@ snapshots: '@babel/parser': 7.29.0 '@babel/template': 7.28.6 '@babel/types': 7.29.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -4109,6 +4172,44 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@clerk/backend@3.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@clerk/shared': 4.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + standardwebhooks: 1.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-dom + + '@clerk/nextjs@7.0.1(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@clerk/backend': 3.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/react': 6.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/shared': 4.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + server-only: 0.0.1 + tslib: 2.8.1 + + '@clerk/react@6.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@clerk/shared': 4.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tslib: 2.8.1 + + '@clerk/shared@4.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@tanstack/query-core': 5.90.16 + dequal: 2.0.3 + glob-to-regexp: 0.4.1 + js-cookie: 3.0.5 + std-env: 3.10.0 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@dotenvx/dotenvx@1.52.0': dependencies: commander: 11.1.0 @@ -4141,17 +4242,17 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3(jiti@2.6.1))': dependencies: - eslint: 9.39.3(jiti@2.6.1)(supports-color@7.2.0) + eslint: 9.39.3(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.1(supports-color@7.2.0)': + '@eslint/config-array@0.21.1': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -4164,10 +4265,10 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.4(supports-color@7.2.0)': + '@eslint/eslintrc@3.3.4': dependencies: ajv: 6.14.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -4363,7 +4464,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@modelcontextprotocol/sdk@1.27.1(supports-color@7.2.0)(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.11(hono@4.12.5) ajv: 8.18.0 @@ -4373,8 +4474,8 @@ snapshots: cross-spawn: 7.0.6 eventsource: 3.0.7 eventsource-parser: 3.0.6 - express: 5.2.1(supports-color@7.2.0) - express-rate-limit: 8.2.1(express@5.2.1(supports-color@7.2.0)) + express: 5.2.1 + express-rate-limit: 8.2.1(express@5.2.1) hono: 4.12.5 jose: 6.2.0 json-schema-typed: 8.0.2 @@ -5227,6 +5328,8 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@stablelib/base64@1.0.1': {} + '@standard-schema/spec@1.1.0': {} '@standard-schema/utils@0.3.0': {} @@ -5304,6 +5407,8 @@ snapshots: postcss: 8.5.8 tailwindcss: 4.2.1 + '@tanstack/query-core@5.90.16': {} + '@ts-morph/common@0.27.0': dependencies: fast-glob: 3.3.3 @@ -5363,15 +5468,15 @@ snapshots: '@types/validate-npm-package-name@4.0.2': {} - '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.1 - eslint: 9.39.3(jiti@2.6.1)(supports-color@7.2.0) + eslint: 9.39.3(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -5379,23 +5484,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.1 - debug: 4.4.3(supports-color@7.2.0) - eslint: 9.39.3(jiti@2.6.1)(supports-color@7.2.0) + debug: 4.4.3 + eslint: 9.39.3(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.56.1(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) '@typescript-eslint/types': 8.56.1 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5409,13 +5514,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) - debug: 4.4.3(supports-color@7.2.0) - eslint: 9.39.3(jiti@2.6.1)(supports-color@7.2.0) + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.3(jiti@2.6.1) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -5423,13 +5528,13 @@ snapshots: '@typescript-eslint/types@8.56.1': {} - '@typescript-eslint/typescript-estree@8.56.1(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.56.1(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) '@typescript-eslint/types': 8.56.1 '@typescript-eslint/visitor-keys': 8.56.1 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 minimatch: 10.2.4 semver: 7.7.4 tinyglobby: 0.2.15 @@ -5438,13 +5543,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + '@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(supports-color@7.2.0)(typescript@5.9.3) - eslint: 9.39.3(jiti@2.6.1)(supports-color@7.2.0) + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + eslint: 9.39.3(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5651,11 +5756,11 @@ snapshots: baseline-browser-mapping@2.10.0: {} - body-parser@2.2.2(supports-color@7.2.0): + body-parser@2.2.2: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 @@ -5852,17 +5957,13 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - debug@3.2.7(supports-color@7.2.0): + debug@3.2.7: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 7.2.0 - debug@4.4.3(supports-color@7.2.0): + debug@4.4.3: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 7.2.0 decimal.js-light@2.5.1: {} @@ -5895,6 +5996,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -6052,18 +6155,18 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.1.6(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + eslint-config-next@16.1.6(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 16.1.6 - eslint: 9.39.3(jiti@2.6.1)(supports-color@7.2.0) - eslint-import-resolver-node: 0.3.9(supports-color@7.2.0) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0)) - eslint-plugin-react: 7.37.5(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0)) - eslint-plugin-react-hooks: 7.0.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0) + eslint: 9.39.3(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.3(jiti@2.6.1)) + eslint-plugin-react-hooks: 7.0.1(eslint@9.39.3(jiti@2.6.1)) globals: 16.4.0 - typescript-eslint: 8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + typescript-eslint: 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -6072,52 +6175,52 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-import-resolver-node@0.3.9(supports-color@7.2.0): + eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7(supports-color@7.2.0) + debug: 3.2.7 is-core-module: 2.16.1 resolve: 1.22.11 transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3(supports-color@7.2.0) - eslint: 9.39.3(jiti@2.6.1)(supports-color@7.2.0) + debug: 4.4.3 + eslint: 9.39.3(jiti@2.6.1) get-tsconfig: 4.13.6 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)): dependencies: - debug: 3.2.7(supports-color@7.2.0) + debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) - eslint: 9.39.3(jiti@2.6.1)(supports-color@7.2.0) - eslint-import-resolver-node: 0.3.9(supports-color@7.2.0) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.3(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 - debug: 3.2.7(supports-color@7.2.0) + debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.3(jiti@2.6.1)(supports-color@7.2.0) - eslint-import-resolver-node: 0.3.9(supports-color@7.2.0) - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0) + eslint: 9.39.3(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -6129,13 +6232,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.3(jiti@2.6.1)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -6145,7 +6248,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.3(jiti@2.6.1)(supports-color@7.2.0) + eslint: 9.39.3(jiti@2.6.1) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -6154,18 +6257,18 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@7.0.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0): + eslint-plugin-react-hooks@7.0.1(eslint@9.39.3(jiti@2.6.1)): dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/parser': 7.29.0 - eslint: 9.39.3(jiti@2.6.1)(supports-color@7.2.0) + eslint: 9.39.3(jiti@2.6.1) hermes-parser: 0.25.1 zod: 4.3.6 zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color - eslint-plugin-react@7.37.5(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0)): + eslint-plugin-react@7.37.5(eslint@9.39.3(jiti@2.6.1)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -6173,7 +6276,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.2 - eslint: 9.39.3(jiti@2.6.1)(supports-color@7.2.0) + eslint: 9.39.3(jiti@2.6.1) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -6198,14 +6301,14 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0): + eslint@9.39.3(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1(supports-color@7.2.0) + '@eslint/config-array': 0.21.1 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.4(supports-color@7.2.0) + '@eslint/eslintrc': 3.3.4 '@eslint/js': 9.39.3 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 @@ -6215,7 +6318,7 @@ snapshots: ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -6296,25 +6399,25 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 - express-rate-limit@8.2.1(express@5.2.1(supports-color@7.2.0)): + express-rate-limit@8.2.1(express@5.2.1): dependencies: - express: 5.2.1(supports-color@7.2.0) + express: 5.2.1 ip-address: 10.0.1 - express@5.2.1(supports-color@7.2.0): + express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.2(supports-color@7.2.0) + body-parser: 2.2.2 content-disposition: 1.0.1 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.1(supports-color@7.2.0) + finalhandler: 2.1.1 fresh: 2.0.0 http-errors: 2.0.1 merge-descriptors: 2.0.0 @@ -6325,9 +6428,9 @@ snapshots: proxy-addr: 2.0.7 qs: 6.15.0 range-parser: 1.2.1 - router: 2.2.0(supports-color@7.2.0) - send: 1.2.1(supports-color@7.2.0) - serve-static: 2.2.1(supports-color@7.2.0) + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 @@ -6356,6 +6459,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-sha256@1.3.0: {} + fast-uri@3.1.0: {} fastq@1.20.1: @@ -6383,9 +6488,9 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.1(supports-color@7.2.0): + finalhandler@2.1.1: dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -6496,6 +6601,8 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-to-regexp@0.4.1: {} + globals@14.0.0: {} globals@16.4.0: {} @@ -6551,10 +6658,10 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - https-proxy-agent@7.0.6(supports-color@7.2.0): + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -6762,6 +6869,8 @@ snapshots: jose@6.2.0: {} + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -6975,7 +7084,7 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - next@16.1.6(@babel/core@7.29.0(supports-color@7.2.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@next/env': 16.1.6 '@swc/helpers': 0.5.15 @@ -6984,7 +7093,7 @@ snapshots: postcss: 8.4.31 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - styled-jsx: 5.1.6(@babel/core@7.29.0(supports-color@7.2.0))(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.3) optionalDependencies: '@next/swc-darwin-arm64': 16.1.6 '@next/swc-darwin-x64': 16.1.6 @@ -7431,9 +7540,9 @@ snapshots: reusify@1.1.0: {} - router@2.2.0(supports-color@7.2.0): + router@2.2.0: dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -7474,9 +7583,9 @@ snapshots: semver@7.7.4: {} - send@1.2.1(supports-color@7.2.0): + send@1.2.1: dependencies: - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -7490,15 +7599,17 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@2.2.1(supports-color@7.2.0): + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.1(supports-color@7.2.0) + send: 1.2.1 transitivePeerDependencies: - supports-color + server-only@0.0.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -7523,15 +7634,15 @@ snapshots: setprototypeof@1.2.0: {} - shadcn@3.8.5(@types/node@20.19.35)(supports-color@7.2.0)(typescript@5.9.3): + shadcn@3.8.5(@types/node@20.19.35)(typescript@5.9.3): dependencies: '@antfu/ni': 25.0.0 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/parser': 7.29.0 - '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0) - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) '@dotenvx/dotenvx': 1.52.0 - '@modelcontextprotocol/sdk': 1.27.1(supports-color@7.2.0)(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.27.1(zod@3.25.76) '@types/validate-npm-package-name': 4.0.2 browserslist: 4.28.1 commander: 14.0.3 @@ -7543,7 +7654,7 @@ snapshots: fast-glob: 3.3.3 fs-extra: 11.3.4 fuzzysort: 3.1.0 - https-proxy-agent: 7.0.6(supports-color@7.2.0) + https-proxy-agent: 7.0.6 kleur: 4.1.5 msw: 2.12.10(@types/node@20.19.35)(typescript@5.9.3) node-fetch: 3.3.2 @@ -7650,8 +7761,15 @@ snapshots: stable-hash@0.0.5: {} + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + statuses@2.0.2: {} + std-env@3.10.0: {} + stdin-discarder@0.2.2: {} stop-iteration-iterator@1.1.0: @@ -7745,12 +7863,12 @@ snapshots: strip-json-comments@3.1.1: {} - styled-jsx@5.1.6(@babel/core@7.29.0(supports-color@7.2.0))(react@19.2.3): + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.3): dependencies: client-only: 0.0.1 react: 19.2.3 optionalDependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 supports-color@7.2.0: dependencies: @@ -7864,13 +7982,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + typescript-eslint@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.56.1(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) - eslint: 9.39.3(jiti@2.6.1)(supports-color@7.2.0) + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.3(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index 4f7bed0..581a9d5 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -1,19 +1,3 @@ -# pnpm refuses to install (ERR_PNPM_IGNORED_BUILDS) if a dependency has a -# postinstall script that is neither approved nor explicitly declined. -# -# `allowBuilds` is the pnpm 11 spelling; `ignoredBuiltDependencies` is kept for -# pnpm 10, which does not understand `allowBuilds`. Both must list the same -# packages. None of these are needed to run `next build`: -# sharp — image-optimisation binary; unused, the app has no next/image -# unrs-resolver — native resolver for eslint-config-next (lint only) -# msw — pulled in by the `shadcn` CLI; its postinstall only writes a -# browser service worker -allowBuilds: - msw: false - sharp: false - unrs-resolver: false - ignoredBuiltDependencies: - sharp - unrs-resolver - - msw diff --git a/infra/mosquitto/Dockerfile b/infra/mosquitto/Dockerfile deleted file mode 100644 index 1ee8a31..0000000 --- a/infra/mosquitto/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -# Mosquitto with the broker config baked in. -# -# The compose file bind-mounts ./infra/mosquitto/mosquitto.conf, which only works -# when the repo is checked out on the Docker host. The Unraid deploy builds images -# remotely via the Portainer Docker API and has no repo on the host, so the config -# travels inside the image instead. -FROM eclipse-mosquitto:2 - -COPY mosquitto.conf /mosquitto/config/mosquitto.conf - -EXPOSE 1883 diff --git a/memory.md b/memory.md deleted file mode 100644 index fc0256a..0000000 --- a/memory.md +++ /dev/null @@ -1,85 +0,0 @@ -# BMS — memory - -DCIM demo platform: Next.js 16 frontend, FastAPI backend, TimescaleDB, Mosquitto MQTT, -and Python simulator bots that publish fake data-centre telemetry. - -Live at **http://192.168.1.249:5646/dashboard** (Portainer stack `bms` on Unraid). - -## Decisions & rationale - -- **Auth is disabled (demo mode).** The repo shipped half-converted: `proxy.ts` already said - "auth is disabled for demo mode", but `app/layout.tsx` still wrapped the tree in - `` and `topbar.tsx` used ``. `@clerk/nextjs` v7 fails `next build` - without a real publishable key, so the frontend image could not build at all. Finished the - conversion instead of buying Clerk keys: removed `ClerkProvider`/`UserButton`, deleted the - `sign-in`/`sign-up` route groups, and dropped `@clerk/nextjs` from `package.json`. The service - is LAN-only and reachable remotely over Tailscale, so the tailnet is the auth boundary. -- **Deploy via the Portainer API, not compose-on-host.** No `docker` CLI in WSL and Unraid's - SSH (port 22) is closed, so `ops/deploy.py` tars each service's build context, POSTs it to the - remote daemon's `/build` endpoint, then pushes `docker-compose.prod.yml` as a Portainer stack. - Same pattern as `toknmtr/ops/deploy.py`. -- **Separate `docker-compose.prod.yml`.** The dev compose file bind-mounts source into the - containers and runs `uvicorn --reload` — wrong for a long-running deploy, and impossible - anyway since the repo is not checked out on the Unraid host. The prod file references - prebuilt image tags, sets env inline (no `.env` files on the host), and publishes only the - frontend's port. -- **Mosquitto config is baked into an image** (`infra/mosquitto/Dockerfile`) rather than - bind-mounted, for the same reason: no repo on the host. -- **`bms_net` pins subnet 172.31.42.0/24.** Docker's default address pools on that host are - fully subnetted — 24 existing stacks have claimed every `172.17-172.31/16` and - `192.168.x/20` slot, so auto-allocation fails with "all predefined address pools have been - fully subnetted". An explicit subnet bypasses the allocator without disturbing other stacks. - Only other 172.31 tenant is `wg0` on 172.31.200.0/24. -- **Postgres data bind-mounts to `/mnt/user/appdata/bms/db`**, not a named volume, so a - growing timeseries DB lands on the array instead of filling Unraid's fixed-size `docker.img`. -- **`pnpm-workspace.yaml` must be copied in the frontend Dockerfile's deps stage.** It carries - the build-script allowlist; without it pnpm aborts with `ERR_PNPM_IGNORED_BUILDS`. The file - now declares both `allowBuilds` (pnpm 11 spelling) and `ignoredBuiltDependencies` (pnpm 10). - `sharp`, `unrs-resolver` and `msw` are all declined — the app has no `next/image`, so sharp - is dead weight. `packageManager: pnpm@11.20.0` is pinned so a future pnpm release cannot - silently change this again. - -## Open questions / TODOs - -- [ ] Postgres uses the repo's default credentials (`dcim`/`dcim_pass`). Fine while the DB has - no published host port and the box is LAN-only, but change both the compose env and - `DATABASE_URL` together if the port is ever exposed. -- [ ] The simulator floods logs with `mqtt WARNING There are N pending publish calls.` — it - publishes faster than aiomqtt drains. Cosmetic, but it makes `docker logs` near-useless - and grows the log file. Worth batching or throttling the bots. -- [ ] `backend/api/routes/ws.py` exists but nothing in the frontend opens a WebSocket (the UI - polls every 15 s). Either wire it up or drop it. Note: Next.js rewrites do not proxy WS - upgrades, so a WS client would need to reach the backend directly. -- [ ] No host port is published for Postgres or MQTT. Add a `ports:` entry to - `docker-compose.prod.yml` if direct DB access or external MQTT publishing is ever needed. -- [ ] No reverse-proxy entry yet. Add an Nginx Proxy Manager host pointing at `:5646` if this - should be reachable by name rather than IP:port. - -## Session log - -### 2026-08-04 - -- Cloned the Forgejo repo into `~/claude/projects/BMS` and deployed it to the Unraid host. -- Finished the demo-mode auth conversion (see Decisions) so the frontend image could build; - regenerated `pnpm-lock.yaml` without `@clerk/nextjs`. Verified `next build` locally — all - 18 routes prerender. -- Hit and fixed three deploy blockers in order: (1) the frontend Dockerfile never copied - `pnpm-workspace.yaml`, so pnpm's ignored-builds allowlist was invisible inside the build; - (2) pnpm 11 renamed that setting to `allowBuilds` and ignores `ignoredBuiltDependencies`; - (3) the Unraid daemon's Docker address pools were exhausted, so the stack network could not - be created. -- Backend's host port 8000 deliberately not published — Portainer's Edge tunnel already owns - 8000 on that host. Ports 1883/5433/5646 were free; only 5646 is published. -- Added `ops/deploy.py`, `docker-compose.prod.yml`, `infra/mosquitto/Dockerfile`; updated the - README (demo-mode note, Unraid deploy section) and both `.env` examples. -- Verified live: all 5 containers healthy, 30 min of seeded history present, telemetry - flowing, alarm engine firing (55 active), dashboard screenshotted and rendering correctly. - -## External references - -- Repo: https://git.rdx4.com/megaproxy/BMS -- Live dashboard: http://192.168.1.249:5646/dashboard -- API docs (Swagger): http://192.168.1.249:5646/api/backend/docs -- Portainer (stack `bms`, endpoint id 3): http://192.168.1.249:9000 — token at `~/.portainer-token` -- Postgres data on the host: `/mnt/user/appdata/bms/db` -- Deploy pattern copied from: `~/claude/projects/toknmtr/ops/deploy.py` diff --git a/ops/deploy.py b/ops/deploy.py deleted file mode 100644 index 9d874fb..0000000 --- a/ops/deploy.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/env python3 -"""Deploy the BMS stack to the Unraid Docker host via the Portainer API. - -Why this shape: there is no usable `docker` CLI in WSL, no SSH to the Unraid box -(port 22 is closed) and no shared registry the Unraid daemon can pull from. So each -service's build *context* is packed into a tar and POSTed to the remote daemon's -`/build` endpoint — Unraid runs the Dockerfiles itself. Once the four images exist -locally on that daemon, docker-compose.prod.yml is pushed as a Portainer stack, -which references the images by tag and never needs the repo on the host. - -Mirrors the pattern in ~/claude/projects/toknmtr/ops/deploy.py. -Idempotent: re-running rebuilds every image and updates the existing stack in place. -The Postgres bind mount at /mnt/user/appdata/bms/db survives redeploys. - -Usage: - python3 ops/deploy.py # build everything + deploy the stack - python3 ops/deploy.py --no-build # redeploy the stack using existing images -""" -import io -import json -import os -import sys -import tarfile -import time -import urllib.error -import urllib.request - -PORTAINER = "http://192.168.1.249:9000" -EP = 3 # 'local' docker endpoint (unix:///var/run/docker.sock) -DOCKER = f"{PORTAINER}/api/endpoints/{EP}/docker" - -HOST = "192.168.1.249" -FRONTEND_PORT = "5646" -STACK_NAME = "bms" -APPDATA = "/mnt/user/appdata/bms" - -PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -COMPOSE_FILE = os.path.join(PROJECT_DIR, "docker-compose.prod.yml") - -# (image tag, context subdirectory) — built in this order. -# mqtt/backend first so the slow frontend build is last and failures surface early. -IMAGES = [ - ("bms-mqtt:latest", "infra/mosquitto"), - ("bms-backend:latest", "backend"), - ("bms-simulators:latest", "simulators"), - ("bms-frontend:latest", "frontend"), -] - -# Never shipped in a build context. -EXCLUDE_DIRS = {".git", "node_modules", ".next", "__pycache__", ".venv", "venv", - ".vscode", ".idea", ".claude"} -EXCLUDE_SUFFIXES = (".pyc", ".pyo", ".log", ".tsbuildinfo") -EXCLUDE_FILES = {".env", ".env.local", ".DS_Store", "Thumbs.db"} - -TOKEN = open(os.path.expanduser("~/.portainer-token")).read().strip() -HDR = {"X-API-Key": TOKEN} - - -def req(url, method, data=None, headers=None, raw=False, timeout=900): - h = dict(HDR) - if headers: - h.update(headers) - body = data - if data is not None and not raw: - body = json.dumps(data).encode() - h["Content-Type"] = "application/json" - r = urllib.request.Request(url, data=body, headers=h, method=method) - try: - with urllib.request.urlopen(r, timeout=timeout) as resp: - return resp.status, resp.read() - except urllib.error.HTTPError as e: - return e.code, e.read() - - -def docker(method, path, **kw): - return req(DOCKER + path, method, **kw) - - -def api(method, path, **kw): - return req(PORTAINER + "/api" + path, method, **kw) - - -# ── image builds ───────────────────────────────────────────────────── - -def context_tar(subdir): - """Pack one service directory (minus excludes) for the Docker /build API.""" - root_dir = os.path.join(PROJECT_DIR, subdir) - if not os.path.isdir(root_dir): - sys.exit(f"ERROR: build context {subdir} does not exist.") - if not os.path.exists(os.path.join(root_dir, "Dockerfile")): - sys.exit(f"ERROR: no Dockerfile in {subdir}.") - buf = io.BytesIO() - with tarfile.open(fileobj=buf, mode="w") as tar: - for root, dirs, files in os.walk(root_dir): - dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS] - for fn in files: - if fn in EXCLUDE_FILES or fn.endswith(EXCLUDE_SUFFIXES): - continue - full = os.path.join(root, fn) - tar.add(full, arcname=os.path.relpath(full, root_dir)) - return buf.getvalue() - - -def build_image(tag, subdir): - ctx = context_tar(subdir) - print(f"\n── building {tag} from {subdir}/ ({len(ctx)/1024:.0f} KiB context)") - status, out = docker( - "POST", - f"/build?t={tag}&dockerfile=Dockerfile&forcerm=true", - data=ctx, - headers={"Content-Type": "application/x-tar"}, - raw=True, - timeout=1800, # the Next.js build is the slow one - ) - text = out.decode(errors="replace") - errored = None - last = "" - for line in text.splitlines(): - try: - msg = json.loads(line) - except ValueError: - continue - if "stream" in msg: - s = msg["stream"].rstrip() - if s: - last = s - if s.startswith("Step ") or "Successfully" in s: - print(" " + s) - if "error" in msg: - errored = msg["error"] - if status != 200 or errored: - print(f"BUILD FAILED ({tag}) — status {status}") - print(text[-3000:]) - sys.exit(f"ERROR: build of {tag} failed.") - print(f" OK: {last[:120]}") - - -# ── host prep ──────────────────────────────────────────────────────── - -def ensure_appdata(): - """Create /mnt/user/appdata/bms/db before Postgres bind-mounts it. - - dockerd would auto-create the path, but doing it explicitly turns a silent - permission problem into a visible one. - """ - print(f"\n── ensuring {APPDATA}/db exists on the host") - docker("POST", "/images/create?fromImage=busybox&tag=latest", data=b"", raw=True, - headers={"Content-Type": "application/json"}, timeout=300) - body = { - "Image": "busybox:latest", - "Cmd": ["mkdir", "-p", "/hostdata/bms/db"], - "HostConfig": {"Binds": ["/mnt/user/appdata:/hostdata"], "AutoRemove": True}, - } - status, out = docker("POST", "/containers/create?name=bms_mkdir_oneshot", data=body) - if status not in (200, 201): - print(" warn: could not create helper container:", - out.decode(errors="replace")[:200]) - print(" continuing — dockerd will create the bind path itself") - return - cid = json.loads(out)["Id"] - docker("POST", f"/containers/{cid}/start") - docker("POST", f"/containers/{cid}/wait", data={}, timeout=120) - print(" OK") - - -# ── stack deploy ───────────────────────────────────────────────────── - -def find_stack(): - status, out = api("GET", "/stacks") - if status != 200: - sys.exit(f"ERROR: could not list stacks ({status}): " - f"{out.decode(errors='replace')[:300]}") - for s in json.loads(out): - if s.get("Name") == STACK_NAME: - return s - return None - - -def deploy_stack(): - compose = open(COMPOSE_FILE).read() - existing = find_stack() - - if existing: - sid = existing["Id"] - print(f"\n── updating existing stack '{STACK_NAME}' (id {sid})") - status, out = api( - "PUT", f"/stacks/{sid}?endpointId={EP}", - data={"stackFileContent": compose, "env": existing.get("Env", []), - "prune": True, "pullImage": False}, - timeout=900, - ) - else: - print(f"\n── creating stack '{STACK_NAME}'") - status, out = api( - "POST", f"/stacks/create/standalone/string?endpointId={EP}", - data={"name": STACK_NAME, "stackFileContent": compose, "env": []}, - timeout=900, - ) - if status == 404: - # Portainer < 2.19 route - status, out = api( - "POST", f"/stacks?type=2&method=string&endpointId={EP}", - data={"Name": STACK_NAME, "StackFileContent": compose, "Env": []}, - timeout=900, - ) - - if status not in (200, 201): - print("STACK DEPLOY FAILED — status", status) - print(out.decode(errors="replace")[:3000]) - sys.exit("ERROR: stack deploy failed.") - print(" stack deployed") - - -# ── verify ─────────────────────────────────────────────────────────── - -def container_states(): - status, out = docker("GET", "/containers/json?all=1") - if status != 200: - return {} - return {n.lstrip("/"): (c.get("State"), c.get("Status")) - for c in json.loads(out) for n in c.get("Names", []) - if n.lstrip("/").startswith("bms_")} - - -def verify(): - print("\n── verifying") - home = f"http://{HOST}:{FRONTEND_PORT}/dashboard" - health = f"http://{HOST}:{FRONTEND_PORT}/api/backend/api/health" - ok = False - for i in range(40): - time.sleep(5) - try: - with urllib.request.urlopen(health, timeout=10) as r: - j = json.load(r) - with urllib.request.urlopen(home, timeout=15) as r2: - code = r2.status - print(f" OK after {5*(i+1)}s — backend health={j}, /dashboard HTTP {code}") - ok = True - break - except Exception as e: - print(f" ...not ready ({type(e).__name__}: {str(e)[:70]})") - - print("\n container states:") - for name, (state, st) in sorted(container_states().items()): - print(f" {name:20s} {state:10s} {st}") - - if not ok: - sys.exit("ERROR: stack did not come up healthy in time.") - - -def main(): - if "--no-build" not in sys.argv: - for tag, subdir in IMAGES: - build_image(tag, subdir) - else: - print("Skipping image builds (--no-build).") - ensure_appdata() - deploy_stack() - verify() - print(f"\nDeployed. Dashboard: http://{HOST}:{FRONTEND_PORT}/dashboard") - print(f"API docs: http://{HOST}:{FRONTEND_PORT}/api/backend/docs") - - -if __name__ == "__main__": - main()