summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArseney300 <Arseney300@gmail.com>2026-03-05 09:47:09 +0700
committerArseney300 <Arseney300@gmail.com>2026-03-05 09:47:09 +0700
commit0885d20ac18c252f6735cd288448d5d93e95b80f (patch)
treeedfb6814e97d4000605d9f6a918c30c6aca9e10c
parent22749b796d7a5e4efa9494156a3fdcfe39028da7 (diff)
WIP: admin_panel: add featurefeature/admin_panel
-rw-r--r--.gitignore4
-rw-r--r--admin-panel/.env.example6
-rw-r--r--admin-panel/Dockerfile12
-rw-r--r--admin-panel/docker-compose.yaml33
-rw-r--r--admin-panel/package-lock.json3407
-rw-r--r--admin-panel/package.json28
-rw-r--r--admin-panel/src/client/components/agent-log.ts2
-rw-r--r--admin-panel/src/client/components/board.ts123
-rw-r--r--admin-panel/src/client/components/diff-viewer.ts2
-rw-r--r--admin-panel/src/client/components/pipeline-controls.ts2
-rw-r--r--admin-panel/src/client/components/project-form.ts2
-rw-r--r--admin-panel/src/client/components/task-card.ts40
-rw-r--r--admin-panel/src/client/components/task-modal.ts271
-rw-r--r--admin-panel/src/client/index.html22
-rw-r--r--admin-panel/src/client/lib/api.ts155
-rw-r--r--admin-panel/src/client/lib/markdown.ts39
-rw-r--r--admin-panel/src/client/lib/ws.ts79
-rw-r--r--admin-panel/src/client/main.ts63
-rw-r--r--admin-panel/src/client/pages/dashboard.ts150
-rw-r--r--admin-panel/src/client/pages/project.ts167
-rw-r--r--admin-panel/src/client/pages/settings.ts59
-rw-r--r--admin-panel/src/client/styles/main.css724
-rw-r--r--admin-panel/src/server/agents/base-agent.ts83
-rw-r--r--admin-panel/src/server/agents/builder.ts64
-rw-r--r--admin-panel/src/server/agents/critic.ts55
-rw-r--r--admin-panel/src/server/agents/inspector.ts54
-rw-r--r--admin-panel/src/server/agents/planner.ts45
-rw-r--r--admin-panel/src/server/agents/ranger.ts54
-rw-r--r--admin-panel/src/server/agents/shield.ts62
-rw-r--r--admin-panel/src/server/db.ts82
-rw-r--r--admin-panel/src/server/index.ts46
-rw-r--r--admin-panel/src/server/routes/dashboard.ts138
-rw-r--r--admin-panel/src/server/routes/gerrit.ts68
-rw-r--r--admin-panel/src/server/routes/pipeline.ts82
-rw-r--r--admin-panel/src/server/routes/projects.ts138
-rw-r--r--admin-panel/src/server/routes/settings.ts35
-rw-r--r--admin-panel/src/server/routes/tasks.ts248
-rw-r--r--admin-panel/src/server/schema.sql73
-rw-r--r--admin-panel/src/server/services/claude.service.ts47
-rw-r--r--admin-panel/src/server/services/gerrit.service.ts81
-rw-r--r--admin-panel/src/server/services/git.service.ts142
-rw-r--r--admin-panel/src/server/services/pipeline.service.ts304
-rw-r--r--admin-panel/src/server/templates/builder.md47
-rw-r--r--admin-panel/src/server/templates/critic.md47
-rw-r--r--admin-panel/src/server/templates/inspector.md61
-rw-r--r--admin-panel/src/server/templates/planner.md48
-rw-r--r--admin-panel/src/server/templates/ranger.md50
-rw-r--r--admin-panel/src/server/templates/shield.md40
-rw-r--r--admin-panel/src/server/ws.ts56
-rw-r--r--admin-panel/tsconfig.json16
-rw-r--r--admin-panel/tsconfig.server.json18
-rw-r--r--admin-panel/vite.config.ts24
-rw-r--r--nginx/docker-compose.yaml4
-rw-r--r--nginx/nginx.conf81
54 files changed, 7782 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore
index c52ba27..4eb3772 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,9 @@
.env
+stack.env
doc/servers.drawio
doc/Servers.drawio
authelia/config/users_database.yml
git-server/gerrit.config
+admin-panel/node_modules/
+admin-panel/dist/
+admin-panel/.env
diff --git a/admin-panel/.env.example b/admin-panel/.env.example
new file mode 100644
index 0000000..4588b0f
--- /dev/null
+++ b/admin-panel/.env.example
@@ -0,0 +1,6 @@
+ANTHROPIC_API_KEY=sk-ant-...
+NODE_ENV=production
+GIT_REPO_PATH=/repos
+GERRIT_URL=http://gerrit:8080
+GERRIT_SSH_HOST=gerrit
+GERRIT_SSH_PORT=29418
diff --git a/admin-panel/Dockerfile b/admin-panel/Dockerfile
new file mode 100644
index 0000000..0e1e27a
--- /dev/null
+++ b/admin-panel/Dockerfile
@@ -0,0 +1,12 @@
+FROM node:22-alpine
+
+RUN apk add --no-cache git openssh-client
+
+WORKDIR /app
+COPY package.json package-lock.json ./
+RUN npm ci --production=false
+COPY . .
+RUN npm run build
+
+EXPOSE 3000
+CMD ["node", "dist/server/index.js"]
diff --git a/admin-panel/docker-compose.yaml b/admin-panel/docker-compose.yaml
new file mode 100644
index 0000000..6df3c85
--- /dev/null
+++ b/admin-panel/docker-compose.yaml
@@ -0,0 +1,33 @@
+services:
+ admin-panel:
+ build: .
+ container_name: admin-panel
+ restart: always
+ environment:
+ - NODE_ENV=production
+ - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
+ - GIT_REPO_PATH=/repos
+ - GERRIT_URL=http://gerrit:8080
+ - GERRIT_SSH_HOST=gerrit
+ - GERRIT_SSH_PORT=29418
+ volumes:
+ - /var/admin-panel/data:/app/data
+ - /var/admin-panel/workspaces:/app/workspaces
+ - /var/git/repos:/repos
+ - /var/admin-panel/ssh:/app/.ssh:ro
+ networks:
+ admin_network:
+ ipv4_address: 172.27.0.2
+ git-network:
+ ipv4_address: 172.22.0.5
+
+networks:
+ admin_network:
+ name: admin_network
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 172.27.0.0/16
+ gateway: 172.27.0.1
+ git-network:
+ external: true
diff --git a/admin-panel/package-lock.json b/admin-panel/package-lock.json
new file mode 100644
index 0000000..a14388d
--- /dev/null
+++ b/admin-panel/package-lock.json
@@ -0,0 +1,3407 @@
+{
+ "name": "admin-panel",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "admin-panel",
+ "version": "1.0.0",
+ "dependencies": {
+ "@anthropic-ai/sdk": "^0.39.0",
+ "better-sqlite3": "^11.7.0",
+ "express": "^4.21.0",
+ "simple-git": "^3.27.0",
+ "ws": "^8.18.0"
+ },
+ "devDependencies": {
+ "@types/better-sqlite3": "^7.6.12",
+ "@types/express": "^5.0.0",
+ "@types/node": "^22.10.0",
+ "@types/ws": "^8.5.13",
+ "concurrently": "^9.1.0",
+ "tsx": "^4.19.0",
+ "typescript": "^5.7.0",
+ "vite": "^6.0.0"
+ }
+ },
+ "node_modules/@anthropic-ai/sdk": {
+ "version": "0.39.0",
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz",
+ "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==",
+ "dependencies": {
+ "@types/node": "^18.11.18",
+ "@types/node-fetch": "^2.6.4",
+ "abort-controller": "^3.0.0",
+ "agentkeepalive": "^4.2.1",
+ "form-data-encoder": "1.7.2",
+ "formdata-node": "^4.3.2",
+ "node-fetch": "^2.6.7"
+ }
+ },
+ "node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
+ "version": "18.19.130",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
+ "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "node_modules/@anthropic-ai/sdk/node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
+ "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
+ "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
+ "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
+ "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
+ "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
+ "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
+ "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
+ "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
+ "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
+ "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
+ "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
+ "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
+ "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
+ "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
+ "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
+ "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
+ "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
+ "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
+ "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
+ "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
+ "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@kwsites/file-exists": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
+ "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
+ "dependencies": {
+ "debug": "^4.1.1"
+ }
+ },
+ "node_modules/@kwsites/file-exists/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@kwsites/file-exists/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/@kwsites/promise-deferred": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
+ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/better-sqlite3": {
+ "version": "7.6.13",
+ "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
+ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true
+ },
+ "node_modules/@types/express": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
+ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
+ "dev": true,
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^5.0.0",
+ "@types/serve-static": "^2"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
+ "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.13",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz",
+ "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/node-fetch": {
+ "version": "2.6.13",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
+ "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^4.0.4"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
+ "dev": true
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/better-sqlite3": {
+ "version": "11.10.0",
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
+ "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "prebuild-install": "^7.1.1"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
+ },
+ "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,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/concurrently": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
+ "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "4.1.2",
+ "rxjs": "7.8.2",
+ "shell-quote": "1.8.3",
+ "supports-color": "8.1.1",
+ "tree-kill": "1.2.2",
+ "yargs": "17.7.2"
+ },
+ "bin": {
+ "conc": "dist/bin/concurrently.js",
+ "concurrently": "dist/bin/concurrently.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+ },
+ "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
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "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",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
+ "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.3",
+ "@esbuild/android-arm": "0.27.3",
+ "@esbuild/android-arm64": "0.27.3",
+ "@esbuild/android-x64": "0.27.3",
+ "@esbuild/darwin-arm64": "0.27.3",
+ "@esbuild/darwin-x64": "0.27.3",
+ "@esbuild/freebsd-arm64": "0.27.3",
+ "@esbuild/freebsd-x64": "0.27.3",
+ "@esbuild/linux-arm": "0.27.3",
+ "@esbuild/linux-arm64": "0.27.3",
+ "@esbuild/linux-ia32": "0.27.3",
+ "@esbuild/linux-loong64": "0.27.3",
+ "@esbuild/linux-mips64el": "0.27.3",
+ "@esbuild/linux-ppc64": "0.27.3",
+ "@esbuild/linux-riscv64": "0.27.3",
+ "@esbuild/linux-s390x": "0.27.3",
+ "@esbuild/linux-x64": "0.27.3",
+ "@esbuild/netbsd-arm64": "0.27.3",
+ "@esbuild/netbsd-x64": "0.27.3",
+ "@esbuild/openbsd-arm64": "0.27.3",
+ "@esbuild/openbsd-x64": "0.27.3",
+ "@esbuild/openharmony-arm64": "0.27.3",
+ "@esbuild/sunos-x64": "0.27.3",
+ "@esbuild/win32-arm64": "0.27.3",
+ "@esbuild/win32-ia32": "0.27.3",
+ "@esbuild/win32-x64": "0.27.3"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data-encoder": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
+ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="
+ },
+ "node_modules/formdata-node": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
+ "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
+ "dependencies": {
+ "node-domexception": "1.0.0",
+ "web-streams-polyfill": "4.0.0-beta.3"
+ },
+ "engines": {
+ "node": ">= 12.20"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "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=="
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.13.6",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
+ "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
+ "dev": true,
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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==",
+ "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/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
+ },
+ "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==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-abi": {
+ "version": "3.87.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
+ "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "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==",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "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,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+ "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/simple-git": {
+ "version": "3.32.3",
+ "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.32.3.tgz",
+ "integrity": "sha512-56a5oxFdWlsGygOXHWrG+xjj5w9ZIt2uQbzqiIGdR/6i5iococ7WQ/bNPzWxCJdEUGUCmyMH0t9zMpRJTaKxmw==",
+ "dependencies": {
+ "@kwsites/file-exists": "^1.1.1",
+ "@kwsites/promise-deferred": "^1.1.1",
+ "debug": "^4.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/steveukx/git-js?sponsor=1"
+ }
+ },
+ "node_modules/simple-git/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/simple-git/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "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==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true
+ },
+ "node_modules/tsx": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "~0.27.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "engines": {
+ "node": ">= 0.4.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==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/web-streams-polyfill": {
+ "version": "4.0.0-beta.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
+ "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
+ "node_modules/ws": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "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,
+ "engines": {
+ "node": ">=12"
+ }
+ }
+ }
+}
diff --git a/admin-panel/package.json b/admin-panel/package.json
new file mode 100644
index 0000000..8bc7960
--- /dev/null
+++ b/admin-panel/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "admin-panel",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "concurrently \"tsx watch src/server/index.ts\" \"vite\"",
+ "build": "vite build && tsc -p tsconfig.server.json",
+ "start": "node dist/server/index.js"
+ },
+ "dependencies": {
+ "@anthropic-ai/sdk": "^0.39.0",
+ "better-sqlite3": "^11.7.0",
+ "express": "^4.21.0",
+ "simple-git": "^3.27.0",
+ "ws": "^8.18.0"
+ },
+ "devDependencies": {
+ "@types/better-sqlite3": "^7.6.12",
+ "@types/express": "^5.0.0",
+ "@types/node": "^22.10.0",
+ "@types/ws": "^8.5.13",
+ "concurrently": "^9.1.0",
+ "tsx": "^4.19.0",
+ "typescript": "^5.7.0",
+ "vite": "^6.0.0"
+ }
+}
diff --git a/admin-panel/src/client/components/agent-log.ts b/admin-panel/src/client/components/agent-log.ts
new file mode 100644
index 0000000..5938426
--- /dev/null
+++ b/admin-panel/src/client/components/agent-log.ts
@@ -0,0 +1,2 @@
+// Agent log is inlined in task-modal.ts — this file exists for Phase 3 expansion
+export {};
diff --git a/admin-panel/src/client/components/board.ts b/admin-panel/src/client/components/board.ts
new file mode 100644
index 0000000..b611133
--- /dev/null
+++ b/admin-panel/src/client/components/board.ts
@@ -0,0 +1,123 @@
+import { api, type Task } from '../lib/api.js';
+import { renderTaskCard } from './task-card.js';
+import { showTaskModal } from './task-modal.js';
+
+const COLUMNS = [
+ { id: 'todo', label: 'Req' },
+ { id: 'plan', label: 'Plan' },
+ { id: 'plan_review', label: 'Plan Review' },
+ { id: 'impl', label: 'Impl' },
+ { id: 'impl_review', label: 'Impl Review' },
+ { id: 'test', label: 'Test' },
+ { id: 'done', label: 'Done' },
+];
+
+export function renderBoard(container: HTMLElement, projectName: string, tasks: Task[]): void {
+ const tasksByStatus = new Map<string, Task[]>();
+ for (const col of COLUMNS) {
+ tasksByStatus.set(col.id, []);
+ }
+ for (const task of tasks) {
+ const list = tasksByStatus.get(task.status);
+ if (list) list.push(task);
+ else tasksByStatus.get('todo')!.push(task); // fallback
+ }
+
+ container.innerHTML = `<div class="board-container">${COLUMNS.map(col => {
+ const colTasks = tasksByStatus.get(col.id)!;
+ return `
+ <div class="board-column" data-status="${col.id}">
+ <div class="column-header">
+ <h3>${col.label}</h3>
+ <span class="column-count">${colTasks.length}</span>
+ </div>
+ <div class="column-body" data-status="${col.id}">
+ ${colTasks.map(task => renderTaskCard(task)).join('')}
+ </div>
+ </div>
+ `;
+ }).join('')}</div>`;
+
+ // Setup drag-drop
+ setupDragDrop(container, projectName);
+
+ // Setup card click
+ container.querySelectorAll('.task-card').forEach(card => {
+ card.addEventListener('click', (e) => {
+ // Don't open modal if dragging
+ if ((card as HTMLElement).classList.contains('dragging')) return;
+ const taskId = parseInt((card as HTMLElement).dataset.taskId!);
+ const task = tasks.find(t => t.id === taskId);
+ if (task) showTaskModal(projectName, task);
+ });
+ });
+}
+
+function setupDragDrop(container: HTMLElement, projectName: string): void {
+ const cards = container.querySelectorAll('.task-card');
+ const columnBodies = container.querySelectorAll('.column-body');
+
+ let draggedCard: HTMLElement | null = null;
+
+ cards.forEach(card => {
+ const el = card as HTMLElement;
+ el.draggable = true;
+
+ el.addEventListener('dragstart', (e) => {
+ draggedCard = el;
+ el.classList.add('dragging');
+ (e as DragEvent).dataTransfer!.effectAllowed = 'move';
+ (e as DragEvent).dataTransfer!.setData('text/plain', el.dataset.taskId!);
+ });
+
+ el.addEventListener('dragend', () => {
+ el.classList.remove('dragging');
+ draggedCard = null;
+ columnBodies.forEach(cb => cb.classList.remove('drag-over'));
+ });
+ });
+
+ columnBodies.forEach(body => {
+ body.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ (e as DragEvent).dataTransfer!.dropEffect = 'move';
+ body.classList.add('drag-over');
+ });
+
+ body.addEventListener('dragleave', () => {
+ body.classList.remove('drag-over');
+ });
+
+ body.addEventListener('drop', async (e) => {
+ e.preventDefault();
+ body.classList.remove('drag-over');
+
+ if (!draggedCard) return;
+
+ const taskId = parseInt(draggedCard.dataset.taskId!);
+ const newStatus = (body as HTMLElement).dataset.status!;
+
+ // Calculate rank based on drop position
+ const existingCards = Array.from(body.querySelectorAll('.task-card:not(.dragging)'));
+ let rank: number;
+
+ if (existingCards.length === 0) {
+ rank = 1;
+ } else {
+ // Drop at end by default
+ const lastCard = existingCards[existingCards.length - 1] as HTMLElement;
+ const lastRank = parseFloat(lastCard.dataset.rank || '0');
+ rank = lastRank + 1;
+ }
+
+ // Optimistically move card
+ body.appendChild(draggedCard);
+
+ try {
+ await api.reorderTask(projectName, taskId, rank, newStatus);
+ } catch (err) {
+ console.error('Reorder failed:', err);
+ }
+ });
+ });
+}
diff --git a/admin-panel/src/client/components/diff-viewer.ts b/admin-panel/src/client/components/diff-viewer.ts
new file mode 100644
index 0000000..b746b75
--- /dev/null
+++ b/admin-panel/src/client/components/diff-viewer.ts
@@ -0,0 +1,2 @@
+// Diff viewer is inlined in task-modal.ts — this file exists for Phase 4 expansion
+export {};
diff --git a/admin-panel/src/client/components/pipeline-controls.ts b/admin-panel/src/client/components/pipeline-controls.ts
new file mode 100644
index 0000000..2b3e9a5
--- /dev/null
+++ b/admin-panel/src/client/components/pipeline-controls.ts
@@ -0,0 +1,2 @@
+// Pipeline controls are inlined in task-modal.ts — this file exists for Phase 3 expansion
+export {};
diff --git a/admin-panel/src/client/components/project-form.ts b/admin-panel/src/client/components/project-form.ts
new file mode 100644
index 0000000..d69f194
--- /dev/null
+++ b/admin-panel/src/client/components/project-form.ts
@@ -0,0 +1,2 @@
+// Project form is inlined in dashboard.ts — this file exists for future extraction if needed
+export {};
diff --git a/admin-panel/src/client/components/task-card.ts b/admin-panel/src/client/components/task-card.ts
new file mode 100644
index 0000000..a63e507
--- /dev/null
+++ b/admin-panel/src/client/components/task-card.ts
@@ -0,0 +1,40 @@
+import type { Task } from '../lib/api.js';
+
+export function renderTaskCard(task: Task): string {
+ const tags = parseTags(task.tags);
+ const priorityClass = `badge-${task.priority}`;
+ const isActive = task.current_agent !== null;
+
+ return `
+ <div class="task-card ${isActive ? 'pipeline-active' : ''}"
+ data-task-id="${task.id}"
+ data-rank="${task.rank}"
+ draggable="true">
+ <div class="task-card-header">
+ <span class="task-id">#${task.id}</span>
+ ${task.current_agent ? `<span class="badge badge-agent">${escapeHtml(task.current_agent)}</span>` : ''}
+ </div>
+ <div class="task-title">${escapeHtml(task.title)}</div>
+ <div class="task-meta">
+ <span class="badge ${priorityClass}">${task.priority}</span>
+ <span class="badge badge-level">L${task.level}</span>
+ ${tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')}
+ </div>
+ </div>
+ `;
+}
+
+function parseTags(tagsStr: string): string[] {
+ try {
+ const parsed = JSON.parse(tagsStr);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch {
+ return [];
+ }
+}
+
+function escapeHtml(text: string): string {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
diff --git a/admin-panel/src/client/components/task-modal.ts b/admin-panel/src/client/components/task-modal.ts
new file mode 100644
index 0000000..725d921
--- /dev/null
+++ b/admin-panel/src/client/components/task-modal.ts
@@ -0,0 +1,271 @@
+import { api, type Task } from '../lib/api.js';
+import { renderMarkdown } from '../lib/markdown.js';
+
+const STATUS_ORDER = ['todo', 'plan', 'plan_review', 'impl', 'impl_review', 'test', 'done'];
+const STATUS_LABELS: Record<string, string> = {
+ todo: 'Req', plan: 'Plan', plan_review: 'Plan Review',
+ impl: 'Impl', impl_review: 'Impl Review', test: 'Test', done: 'Done'
+};
+
+const TABS = ['Requirements', 'Plan', 'Implementation', 'Reviews', 'Tests', 'Agent Log'];
+
+export function showTaskModal(projectName: string, task: Task): void {
+ document.querySelector('.modal-overlay')?.remove();
+
+ const overlay = document.createElement('div');
+ overlay.className = 'modal-overlay';
+ overlay.innerHTML = `
+ <div class="modal task-modal">
+ <div class="modal-header">
+ <div class="top-row">
+ <span class="task-id" style="font-size:14px">#${task.id}</span>
+ <button class="btn-icon close-btn" style="font-size:20px">&times;</button>
+ </div>
+ <h2 style="width:100%">${escapeHtml(task.title)}</h2>
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
+ <span class="badge badge-${task.priority}">${task.priority}</span>
+ <span class="badge badge-level">L${task.level}</span>
+ <span class="badge" style="background:var(--border);color:var(--text-secondary)">
+ ${STATUS_LABELS[task.status] || task.status}
+ </span>
+ ${task.current_agent ? `<span class="badge badge-agent">${escapeHtml(task.current_agent)}</span>` : ''}
+ ${parseTags(task.tags).map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')}
+ </div>
+ </div>
+
+ ${renderLifecycleBar(task)}
+
+ <div class="tabs" id="task-tabs">
+ ${TABS.map((tab, i) => `
+ <button class="tab ${i === 0 ? 'active' : ''}" data-tab="${i}">${tab}</button>
+ `).join('')}
+ </div>
+
+ <div class="tab-content" id="tab-content">
+ ${renderTabContent(0, task)}
+ </div>
+
+ <div class="modal-footer">
+ <div class="pipeline-controls">
+ <button class="btn btn-primary btn-sm" id="run-pipeline-btn" title="Run full pipeline">Run Pipeline</button>
+ <button class="btn btn-secondary btn-sm" id="step-pipeline-btn" title="Run next step">Step</button>
+ <button class="btn btn-danger btn-sm" id="stop-pipeline-btn" title="Stop pipeline">Stop</button>
+ </div>
+ <div style="flex:1"></div>
+ <button class="btn btn-danger btn-sm" id="delete-task-btn">Delete</button>
+ </div>
+ </div>
+ `;
+
+ document.body.appendChild(overlay);
+
+ // Tab switching
+ overlay.querySelectorAll('.tab').forEach(tab => {
+ tab.addEventListener('click', () => {
+ overlay.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+ tab.classList.add('active');
+ const tabIdx = parseInt((tab as HTMLElement).dataset.tab!);
+ overlay.querySelector('#tab-content')!.innerHTML = renderTabContent(tabIdx, task);
+ });
+ });
+
+ // Close
+ overlay.querySelectorAll('.close-btn').forEach(btn => {
+ btn.addEventListener('click', () => overlay.remove());
+ });
+ overlay.addEventListener('click', (e) => {
+ if (e.target === overlay) overlay.remove();
+ });
+
+ // Pipeline controls
+ overlay.querySelector('#run-pipeline-btn')?.addEventListener('click', async () => {
+ try {
+ await api.runPipeline(projectName, task.id);
+ } catch (err) {
+ alert((err as Error).message);
+ }
+ });
+
+ overlay.querySelector('#step-pipeline-btn')?.addEventListener('click', async () => {
+ try {
+ await api.stepPipeline(projectName, task.id);
+ } catch (err) {
+ alert((err as Error).message);
+ }
+ });
+
+ overlay.querySelector('#stop-pipeline-btn')?.addEventListener('click', async () => {
+ try {
+ await api.stopPipeline(projectName, task.id);
+ } catch (err) {
+ alert((err as Error).message);
+ }
+ });
+
+ // Delete
+ overlay.querySelector('#delete-task-btn')?.addEventListener('click', async () => {
+ if (!confirm(`Delete task #${task.id}?`)) return;
+ try {
+ await api.deleteTask(projectName, task.id);
+ overlay.remove();
+ } catch (err) {
+ alert((err as Error).message);
+ }
+ });
+}
+
+function renderLifecycleBar(task: Task): string {
+ const currentIdx = STATUS_ORDER.indexOf(task.status);
+ return `
+ <div class="lifecycle-bar">
+ ${STATUS_ORDER.map((s, i) => {
+ let cls = 'lifecycle-step';
+ if (i < currentIdx) cls += ' completed';
+ else if (i === currentIdx) cls += ' current';
+ return `<div class="${cls}" title="${STATUS_LABELS[s]}"></div>`;
+ }).join('')}
+ </div>
+ `;
+}
+
+function renderTabContent(tabIdx: number, task: Task): string {
+ switch (tabIdx) {
+ case 0: // Requirements
+ return `
+ <div style="color:var(--text-secondary);font-size:13px">
+ ${task.description
+ ? `<div>${renderMarkdown(task.description)}</div>`
+ : '<p>No description provided.</p>'
+ }
+ ${task.done_when ? `
+ <h4 style="margin-top:16px;color:var(--text-primary)">Done When</h4>
+ <div>${renderMarkdown(task.done_when)}</div>
+ ` : ''}
+ </div>
+ `;
+
+ case 1: // Plan
+ return `
+ <div style="color:var(--text-secondary);font-size:13px">
+ ${task.plan
+ ? `<div>${renderMarkdown(task.plan)}</div>`
+ : '<p>No plan generated yet. Run the pipeline to create one.</p>'
+ }
+ ${task.decision_log ? `
+ <h4 style="margin-top:16px;color:var(--text-primary)">Decision Log</h4>
+ <div>${renderMarkdown(task.decision_log)}</div>
+ ` : ''}
+ </div>
+ `;
+
+ case 2: // Implementation
+ return `
+ <div style="color:var(--text-secondary);font-size:13px">
+ ${task.implementation_notes
+ ? `<div>${renderMarkdown(task.implementation_notes)}</div>`
+ : '<p>No implementation notes yet.</p>'
+ }
+ ${task.branch_name ? `<p style="margin-top:12px">Branch: <code>${escapeHtml(task.branch_name)}</code></p>` : ''}
+ ${task.diff ? `
+ <h4 style="margin-top:16px;color:var(--text-primary)">Diff</h4>
+ <div class="diff-viewer">${renderDiff(task.diff)}</div>
+ ` : ''}
+ </div>
+ `;
+
+ case 3: // Reviews
+ return `
+ <div style="color:var(--text-secondary);font-size:13px">
+ <h4 style="color:var(--text-primary)">Plan Reviews (${task.plan_review_count})</h4>
+ ${renderJsonArray(task.plan_review_comments, 'No plan reviews yet.')}
+
+ <h4 style="margin-top:16px;color:var(--text-primary)">Code Reviews (${task.impl_review_count})</h4>
+ ${renderJsonArray(task.review_comments, 'No code reviews yet.')}
+
+ ${task.gerrit_change_number ? `
+ <p style="margin-top:16px">Gerrit Change: <a href="/gerrit/${task.gerrit_change_number}" style="color:var(--accent-glow)">#${task.gerrit_change_number}</a></p>
+ ` : ''}
+ </div>
+ `;
+
+ case 4: // Tests
+ return `
+ <div style="color:var(--text-secondary);font-size:13px">
+ ${renderJsonArray(task.test_results, 'No test results yet. Run the pipeline to generate tests.')}
+ </div>
+ `;
+
+ case 5: // Agent Log
+ return `
+ <div class="agent-log">
+ ${renderAgentLog(task.agent_log)}
+ </div>
+ `;
+
+ default:
+ return '';
+ }
+}
+
+function renderDiff(diff: string): string {
+ return diff.split('\n').map(line => {
+ let cls = 'diff-line';
+ if (line.startsWith('+')) cls += ' diff-add';
+ else if (line.startsWith('-')) cls += ' diff-del';
+ else if (line.startsWith('@@')) cls += ' diff-hunk';
+ return `<div class="${cls}">${escapeHtml(line)}</div>`;
+ }).join('');
+}
+
+function renderJsonArray(jsonStr: string, emptyMsg: string): string {
+ try {
+ const items = JSON.parse(jsonStr);
+ if (!Array.isArray(items) || items.length === 0) return `<p>${emptyMsg}</p>`;
+ return items.map((item: unknown) => {
+ if (typeof item === 'string') return `<div class="log-entry"><div class="log-message">${escapeHtml(item)}</div></div>`;
+ if (typeof item === 'object' && item !== null) {
+ const obj = item as Record<string, unknown>;
+ return `<div class="log-entry">
+ ${obj.agent ? `<div class="log-agent">${escapeHtml(String(obj.agent))}</div>` : ''}
+ <div class="log-message">${escapeHtml(String(obj.message || obj.comment || JSON.stringify(obj)))}</div>
+ </div>`;
+ }
+ return '';
+ }).join('');
+ } catch {
+ return `<p>${emptyMsg}</p>`;
+ }
+}
+
+function renderAgentLog(logStr: string): string {
+ try {
+ const entries = JSON.parse(logStr);
+ if (!Array.isArray(entries) || entries.length === 0) {
+ return '<div class="empty-state"><p>No agent activity yet.</p></div>';
+ }
+ return entries.map((entry: Record<string, unknown>) => `
+ <div class="log-entry">
+ <span class="log-time">${entry.timestamp ? new Date(String(entry.timestamp)).toLocaleTimeString() : ''}</span>
+ <span class="log-agent">${escapeHtml(String(entry.agent || ''))}</span>
+ <span class="log-message">${escapeHtml(String(entry.message || ''))}</span>
+ </div>
+ `).join('');
+ } catch {
+ return '<div class="empty-state"><p>No agent activity yet.</p></div>';
+ }
+}
+
+function parseTags(tagsStr: string): string[] {
+ try {
+ const parsed = JSON.parse(tagsStr);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch {
+ return [];
+ }
+}
+
+function escapeHtml(text: string): string {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
diff --git a/admin-panel/src/client/index.html b/admin-panel/src/client/index.html
new file mode 100644
index 0000000..58af369
--- /dev/null
+++ b/admin-panel/src/client/index.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Bastion Admin</title>
+ <link rel="stylesheet" href="./styles/main.css" />
+</head>
+<body>
+ <div id="app">
+ <nav id="navbar">
+ <a href="/" class="nav-brand">BASTION</a>
+ <div class="nav-links">
+ <a href="/" data-route="/">Dashboard</a>
+ <a href="/settings" data-route="/settings">Settings</a>
+ </div>
+ </nav>
+ <main id="main-content"></main>
+ </div>
+ <script type="module" src="./main.ts"></script>
+</body>
+</html>
diff --git a/admin-panel/src/client/lib/api.ts b/admin-panel/src/client/lib/api.ts
new file mode 100644
index 0000000..d82693f
--- /dev/null
+++ b/admin-panel/src/client/lib/api.ts
@@ -0,0 +1,155 @@
+const BASE = '/api';
+
+async function request<T>(path: string, options?: RequestInit): Promise<T> {
+ const res = await fetch(`${BASE}${path}`, {
+ headers: { 'Content-Type': 'application/json' },
+ ...options,
+ });
+
+ if (res.status === 204) return undefined as T;
+
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data.error || `Request failed: ${res.status}`);
+ }
+ return data as T;
+}
+
+export const api = {
+ // Projects
+ getProjects: () => request<Project[]>('/projects'),
+ createProject: (data: CreateProject) =>
+ request<Project>('/projects', { method: 'POST', body: JSON.stringify(data) }),
+ getProject: (name: string) => request<Project>(`/projects/${name}`),
+ updateProject: (name: string, data: Partial<Project>) =>
+ request<Project>(`/projects/${name}`, { method: 'PATCH', body: JSON.stringify(data) }),
+ deleteProject: (name: string, deleteRepo = false) =>
+ request<void>(`/projects/${name}?delete_repo=${deleteRepo}`, { method: 'DELETE' }),
+
+ // Tasks
+ getTasks: (project: string) => request<Task[]>(`/projects/${project}/tasks`),
+ createTask: (project: string, data: CreateTask) =>
+ request<Task>(`/projects/${project}/tasks`, { method: 'POST', body: JSON.stringify(data) }),
+ getTask: (project: string, id: number) => request<Task>(`/projects/${project}/tasks/${id}`),
+ updateTask: (project: string, id: number, data: Partial<Task>) =>
+ request<Task>(`/projects/${project}/tasks/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
+ deleteTask: (project: string, id: number) =>
+ request<void>(`/projects/${project}/tasks/${id}`, { method: 'DELETE' }),
+ reorderTask: (project: string, id: number, rank: number, status?: string) =>
+ request<Task>(`/projects/${project}/tasks/${id}/reorder`, {
+ method: 'PATCH',
+ body: JSON.stringify({ rank, status }),
+ }),
+
+ // Pipeline
+ runPipeline: (project: string, taskId: number) =>
+ request<void>(`/projects/${project}/pipeline/run/${taskId}`, { method: 'POST' }),
+ stepPipeline: (project: string, taskId: number) =>
+ request<void>(`/projects/${project}/pipeline/step/${taskId}`, { method: 'POST' }),
+ stopPipeline: (project: string, taskId: number) =>
+ request<void>(`/projects/${project}/pipeline/stop/${taskId}`, { method: 'POST' }),
+ getPipelineStatus: (project: string, taskId: number) =>
+ request<PipelineStatus>(`/projects/${project}/pipeline/status/${taskId}`),
+
+ // Dashboard
+ getDashboardStats: () => request<DashboardStats>('/dashboard/stats'),
+ getDashboardActivity: () => request<ActivityEntry[]>('/dashboard/activity'),
+
+ // Settings
+ getSettings: () => request<Record<string, string>>('/settings'),
+ updateSettings: (data: Record<string, string>) =>
+ request<void>('/settings', { method: 'PATCH', body: JSON.stringify(data) }),
+};
+
+// Types
+export interface Project {
+ id: number;
+ name: string;
+ display_name: string;
+ description: string | null;
+ repo_name: string;
+ repo_path: string;
+ default_branch: string;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface CreateProject {
+ name: string;
+ display_name: string;
+ description?: string;
+}
+
+export interface Task {
+ id: number;
+ title: string;
+ status: string;
+ priority: string;
+ level: number;
+ description: string | null;
+ plan: string | null;
+ decision_log: string | null;
+ done_when: string | null;
+ implementation_notes: string | null;
+ tags: string;
+ plan_review_comments: string;
+ review_comments: string;
+ test_results: string;
+ agent_log: string;
+ current_agent: string | null;
+ plan_review_count: number;
+ impl_review_count: number;
+ branch_name: string | null;
+ gerrit_change_id: string | null;
+ gerrit_change_number: number | null;
+ diff: string | null;
+ rank: number;
+ created_at: string;
+ started_at: string | null;
+ planned_at: string | null;
+ reviewed_at: string | null;
+ tested_at: string | null;
+ completed_at: string | null;
+}
+
+export interface CreateTask {
+ title: string;
+ description?: string;
+ priority?: string;
+ level?: number;
+ tags?: string[];
+}
+
+export interface PipelineStatus {
+ running: boolean;
+ currentAgent: string | null;
+ jobs: PipelineJob[];
+}
+
+export interface PipelineJob {
+ id: number;
+ task_id: number;
+ agent: string;
+ status: string;
+ model: string;
+ started_at: string | null;
+ completed_at: string | null;
+ error: string | null;
+ tokens_used: number;
+}
+
+export interface DashboardStats {
+ totalProjects: number;
+ totalTasks: number;
+ tasksByStatus: Record<string, number>;
+ completedToday: number;
+}
+
+export interface ActivityEntry {
+ project: string;
+ taskId: number;
+ taskTitle: string;
+ agent: string;
+ action: string;
+ timestamp: string;
+}
diff --git a/admin-panel/src/client/lib/markdown.ts b/admin-panel/src/client/lib/markdown.ts
new file mode 100644
index 0000000..c89fa77
--- /dev/null
+++ b/admin-panel/src/client/lib/markdown.ts
@@ -0,0 +1,39 @@
+// Minimal markdown renderer — handles the basics without pulling in a library
+export function renderMarkdown(text: string): string {
+ if (!text) return '';
+
+ let html = escapeHtml(text);
+
+ // Code blocks
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="lang-$1">$2</code></pre>');
+
+ // Inline code
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
+
+ // Headers
+ html = html.replace(/^### (.+)$/gm, '<h4>$1</h4>');
+ html = html.replace(/^## (.+)$/gm, '<h3>$1</h3>');
+ html = html.replace(/^# (.+)$/gm, '<h2>$1</h2>');
+
+ // Bold and italic
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
+
+ // Lists
+ html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
+ html = html.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
+
+ // Line breaks
+ html = html.replace(/\n\n/g, '</p><p>');
+ html = `<p>${html}</p>`;
+ html = html.replace(/<p><\/p>/g, '');
+
+ return html;
+}
+
+function escapeHtml(text: string): string {
+ return text
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+}
diff --git a/admin-panel/src/client/lib/ws.ts b/admin-panel/src/client/lib/ws.ts
new file mode 100644
index 0000000..14e0cec
--- /dev/null
+++ b/admin-panel/src/client/lib/ws.ts
@@ -0,0 +1,79 @@
+type EventHandler = (data: unknown) => void;
+
+class WsClient {
+ private ws: WebSocket | null = null;
+ private handlers = new Map<string, Set<EventHandler>>();
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
+ private subscribedProjects = new Set<string>();
+
+ connect(): void {
+ if (this.ws?.readyState === WebSocket.OPEN) return;
+
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
+ this.ws = new WebSocket(`${protocol}//${location.host}/ws`);
+
+ this.ws.onopen = () => {
+ // Re-subscribe to projects
+ for (const project of this.subscribedProjects) {
+ this.send({ type: 'subscribe', project });
+ }
+ };
+
+ this.ws.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ const handlers = this.handlers.get(data.type);
+ if (handlers) {
+ for (const handler of handlers) {
+ handler(data);
+ }
+ }
+ // Also fire wildcard handlers
+ const wildcardHandlers = this.handlers.get('*');
+ if (wildcardHandlers) {
+ for (const handler of wildcardHandlers) {
+ handler(data);
+ }
+ }
+ } catch {
+ // ignore
+ }
+ };
+
+ this.ws.onclose = () => {
+ this.reconnectTimer = setTimeout(() => this.connect(), 3000);
+ };
+ }
+
+ disconnect(): void {
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
+ this.ws?.close();
+ this.ws = null;
+ }
+
+ subscribe(project: string): void {
+ this.subscribedProjects.add(project);
+ this.send({ type: 'subscribe', project });
+ }
+
+ unsubscribe(project: string): void {
+ this.subscribedProjects.delete(project);
+ this.send({ type: 'unsubscribe', project });
+ }
+
+ on(event: string, handler: EventHandler): () => void {
+ if (!this.handlers.has(event)) {
+ this.handlers.set(event, new Set());
+ }
+ this.handlers.get(event)!.add(handler);
+ return () => this.handlers.get(event)?.delete(handler);
+ }
+
+ private send(data: unknown): void {
+ if (this.ws?.readyState === WebSocket.OPEN) {
+ this.ws.send(JSON.stringify(data));
+ }
+ }
+}
+
+export const wsClient = new WsClient();
diff --git a/admin-panel/src/client/main.ts b/admin-panel/src/client/main.ts
new file mode 100644
index 0000000..87f25db
--- /dev/null
+++ b/admin-panel/src/client/main.ts
@@ -0,0 +1,63 @@
+import { renderDashboard } from './pages/dashboard.js';
+import { renderProjectPage } from './pages/project.js';
+import { renderSettings } from './pages/settings.js';
+import { wsClient } from './lib/ws.js';
+
+const mainContent = document.getElementById('main-content')!;
+
+// Simple client-side router
+type Route = {
+ pattern: RegExp;
+ handler: (container: HTMLElement, params: string[]) => Promise<void>;
+};
+
+const routes: Route[] = [
+ { pattern: /^\/$/, handler: (c) => renderDashboard(c) },
+ { pattern: /^\/project\/([a-z0-9-]+)$/, handler: (c, p) => renderProjectPage(c, p[1]) },
+ { pattern: /^\/settings$/, handler: (c) => renderSettings(c) },
+];
+
+function navigate(path: string): void {
+ for (const route of routes) {
+ const match = path.match(route.pattern);
+ if (match) {
+ updateNavActive(path);
+ route.handler(mainContent, Array.from(match));
+ return;
+ }
+ }
+ // 404
+ mainContent.innerHTML = `
+ <div class="empty-state">
+ <h3>Page not found</h3>
+ <p><a href="/" data-route="/" style="color:var(--accent-glow)">Back to Dashboard</a></p>
+ </div>
+ `;
+}
+
+function updateNavActive(path: string): void {
+ document.querySelectorAll('.nav-links a').forEach(a => {
+ const route = (a as HTMLElement).dataset.route;
+ a.classList.toggle('active', route === path || (route === '/' && path.startsWith('/project/')));
+ });
+}
+
+// Intercept link clicks for SPA navigation
+document.addEventListener('click', (e) => {
+ const link = (e.target as HTMLElement).closest('[data-route]');
+ if (link) {
+ e.preventDefault();
+ const path = (link as HTMLElement).dataset.route!;
+ history.pushState(null, '', path);
+ navigate(path);
+ }
+});
+
+// Handle browser back/forward
+window.addEventListener('popstate', () => {
+ navigate(location.pathname);
+});
+
+// Init
+wsClient.connect();
+navigate(location.pathname);
diff --git a/admin-panel/src/client/pages/dashboard.ts b/admin-panel/src/client/pages/dashboard.ts
new file mode 100644
index 0000000..ded6d3b
--- /dev/null
+++ b/admin-panel/src/client/pages/dashboard.ts
@@ -0,0 +1,150 @@
+import { api, type Project } from '../lib/api.js';
+
+export async function renderDashboard(container: HTMLElement): Promise<void> {
+ container.innerHTML = `
+ <div class="dashboard-header">
+ <h1>Projects</h1>
+ <button class="btn btn-primary" id="new-project-btn">+ New Project</button>
+ </div>
+ <div class="project-grid" id="project-grid">
+ <div class="empty-state">
+ <h3>Loading...</h3>
+ </div>
+ </div>
+ `;
+
+ const grid = container.querySelector('#project-grid')!;
+ const newBtn = container.querySelector('#new-project-btn')!;
+
+ // Load projects
+ try {
+ const projects = await api.getProjects();
+ renderProjects(grid, projects);
+ } catch (err) {
+ grid.innerHTML = `<div class="empty-state"><h3>Failed to load projects</h3><p>${(err as Error).message}</p></div>`;
+ }
+
+ newBtn.addEventListener('click', () => showCreateDialog(grid));
+}
+
+function renderProjects(grid: Element, projects: Project[]): void {
+ if (projects.length === 0) {
+ grid.innerHTML = `
+ <div class="empty-state">
+ <h3>No projects yet</h3>
+ <p>Create your first project to get started.</p>
+ </div>
+ `;
+ return;
+ }
+
+ grid.innerHTML = projects.map(p => `
+ <a href="/project/${p.name}" class="project-card" data-route="/project/${p.name}">
+ <h3>${escapeHtml(p.display_name)}</h3>
+ <div class="project-name">${escapeHtml(p.name)}</div>
+ ${p.description ? `<div class="project-desc">${escapeHtml(p.description)}</div>` : ''}
+ <div class="project-stats">
+ <span class="stat"><span class="stat-dot todo"></span> ${escapeHtml(p.repo_name)}</span>
+ <span class="stat">${timeAgo(p.created_at)}</span>
+ </div>
+ </a>
+ `).join('');
+}
+
+function showCreateDialog(grid: Element): void {
+ // Remove existing modal if present
+ document.querySelector('.modal-overlay')?.remove();
+
+ const overlay = document.createElement('div');
+ overlay.className = 'modal-overlay';
+ overlay.innerHTML = `
+ <div class="modal">
+ <div class="modal-header">
+ <h2>New Project</h2>
+ <button class="btn-icon close-btn">&times;</button>
+ </div>
+ <form id="create-project-form">
+ <div class="form-group">
+ <label for="proj-name">Project Name</label>
+ <input type="text" id="proj-name" placeholder="my-project" required pattern="[a-z0-9-]+" />
+ </div>
+ <div class="form-group">
+ <label for="proj-display">Display Name</label>
+ <input type="text" id="proj-display" placeholder="My Project" required />
+ </div>
+ <div class="form-group">
+ <label for="proj-desc">Description</label>
+ <textarea id="proj-desc" placeholder="What is this project about?"></textarea>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary close-btn">Cancel</button>
+ <button type="submit" class="btn btn-primary">Create Project</button>
+ </div>
+ </form>
+ </div>
+ `;
+
+ document.body.appendChild(overlay);
+
+ // Auto-generate name from display name
+ const displayInput = overlay.querySelector('#proj-display') as HTMLInputElement;
+ const nameInput = overlay.querySelector('#proj-name') as HTMLInputElement;
+ displayInput.addEventListener('input', () => {
+ nameInput.value = displayInput.value
+ .toLowerCase()
+ .replace(/[^a-z0-9\s-]/g, '')
+ .replace(/\s+/g, '-');
+ });
+
+ // Close handlers
+ overlay.querySelectorAll('.close-btn').forEach(btn => {
+ btn.addEventListener('click', () => overlay.remove());
+ });
+ overlay.addEventListener('click', (e) => {
+ if (e.target === overlay) overlay.remove();
+ });
+
+ // Submit
+ const form = overlay.querySelector('#create-project-form') as HTMLFormElement;
+ form.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const submitBtn = form.querySelector('button[type="submit"]') as HTMLButtonElement;
+ submitBtn.disabled = true;
+ submitBtn.textContent = 'Creating...';
+
+ try {
+ await api.createProject({
+ name: nameInput.value,
+ display_name: displayInput.value,
+ description: (overlay.querySelector('#proj-desc') as HTMLTextAreaElement).value || undefined,
+ });
+ overlay.remove();
+ // Reload projects
+ const projects = await api.getProjects();
+ renderProjects(grid, projects);
+ } catch (err) {
+ submitBtn.disabled = false;
+ submitBtn.textContent = 'Create Project';
+ alert((err as Error).message);
+ }
+ });
+
+ displayInput.focus();
+}
+
+function escapeHtml(text: string): string {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+function timeAgo(dateStr: string): string {
+ const date = new Date(dateStr + 'Z');
+ const now = new Date();
+ const diff = Math.floor((now.getTime() - date.getTime()) / 1000);
+
+ if (diff < 60) return 'just now';
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
+ return `${Math.floor(diff / 86400)}d ago`;
+}
diff --git a/admin-panel/src/client/pages/project.ts b/admin-panel/src/client/pages/project.ts
new file mode 100644
index 0000000..5e01d64
--- /dev/null
+++ b/admin-panel/src/client/pages/project.ts
@@ -0,0 +1,167 @@
+import { api, type Task, type Project } from '../lib/api.js';
+import { wsClient } from '../lib/ws.js';
+import { renderBoard } from '../components/board.js';
+
+let currentProject: string | null = null;
+let cleanupWs: (() => void) | null = null;
+
+export async function renderProjectPage(container: HTMLElement, projectName: string): Promise<void> {
+ // Cleanup previous subscriptions
+ if (currentProject && currentProject !== projectName) {
+ wsClient.unsubscribe(currentProject);
+ }
+ if (cleanupWs) cleanupWs();
+
+ currentProject = projectName;
+
+ container.innerHTML = `
+ <div class="board-header">
+ <div style="display:flex;align-items:center;gap:12px">
+ <a href="/" data-route="/" class="btn btn-secondary btn-sm">&larr; Back</a>
+ <h1 id="project-title">Loading...</h1>
+ </div>
+ <button class="btn btn-primary" id="new-task-btn">+ New Task</button>
+ </div>
+ <div id="board-root"></div>
+ `;
+
+ const boardRoot = container.querySelector('#board-root') as HTMLElement;
+ const titleEl = container.querySelector('#project-title')!;
+ const newTaskBtn = container.querySelector('#new-task-btn')!;
+
+ try {
+ const [project, tasks] = await Promise.all([
+ api.getProject(projectName),
+ api.getTasks(projectName),
+ ]);
+
+ titleEl.textContent = project.display_name;
+ renderBoard(boardRoot, projectName, tasks);
+
+ // WebSocket subscription
+ wsClient.subscribe(projectName);
+ const unsub1 = wsClient.on('task:created', (data: unknown) => {
+ const event = data as { project: string; task: Task };
+ if (event.project === projectName) {
+ refreshBoard(boardRoot, projectName);
+ }
+ });
+ const unsub2 = wsClient.on('task:updated', (data: unknown) => {
+ const event = data as { project: string; task: Task };
+ if (event.project === projectName) {
+ refreshBoard(boardRoot, projectName);
+ }
+ });
+ const unsub3 = wsClient.on('task:deleted', (data: unknown) => {
+ const event = data as { project: string };
+ if (event.project === projectName) {
+ refreshBoard(boardRoot, projectName);
+ }
+ });
+
+ cleanupWs = () => { unsub1(); unsub2(); unsub3(); };
+
+ } catch (err) {
+ boardRoot.innerHTML = `<div class="empty-state"><h3>Failed to load project</h3><p>${(err as Error).message}</p></div>`;
+ }
+
+ newTaskBtn.addEventListener('click', () => showCreateTaskDialog(projectName, boardRoot));
+}
+
+async function refreshBoard(boardRoot: HTMLElement, projectName: string): Promise<void> {
+ try {
+ const tasks = await api.getTasks(projectName);
+ renderBoard(boardRoot, projectName, tasks);
+ } catch {
+ // silently fail on refresh
+ }
+}
+
+function showCreateTaskDialog(projectName: string, boardRoot: HTMLElement): void {
+ document.querySelector('.modal-overlay')?.remove();
+
+ const overlay = document.createElement('div');
+ overlay.className = 'modal-overlay';
+ overlay.innerHTML = `
+ <div class="modal">
+ <div class="modal-header">
+ <h2>New Task</h2>
+ <button class="btn-icon close-btn">&times;</button>
+ </div>
+ <form id="create-task-form">
+ <div class="form-group">
+ <label for="task-title">Title</label>
+ <input type="text" id="task-title" placeholder="What needs to be done?" required />
+ </div>
+ <div class="form-group">
+ <label for="task-desc">Description</label>
+ <textarea id="task-desc" rows="4" placeholder="Detailed requirements..."></textarea>
+ </div>
+ <div style="display:flex;gap:12px">
+ <div class="form-group" style="flex:1">
+ <label for="task-priority">Priority</label>
+ <select id="task-priority">
+ <option value="low">Low</option>
+ <option value="medium" selected>Medium</option>
+ <option value="high">High</option>
+ </select>
+ </div>
+ <div class="form-group" style="flex:1">
+ <label for="task-level">Level</label>
+ <select id="task-level">
+ <option value="1">L1 — Quick</option>
+ <option value="2">L2 — Standard</option>
+ <option value="3" selected>L3 — Full</option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group">
+ <label for="task-tags">Tags (comma-separated)</label>
+ <input type="text" id="task-tags" placeholder="frontend, bug-fix" />
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary close-btn">Cancel</button>
+ <button type="submit" class="btn btn-primary">Create Task</button>
+ </div>
+ </form>
+ </div>
+ `;
+
+ document.body.appendChild(overlay);
+
+ overlay.querySelectorAll('.close-btn').forEach(btn => {
+ btn.addEventListener('click', () => overlay.remove());
+ });
+ overlay.addEventListener('click', (e) => {
+ if (e.target === overlay) overlay.remove();
+ });
+
+ const form = overlay.querySelector('#create-task-form') as HTMLFormElement;
+ form.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const submitBtn = form.querySelector('button[type="submit"]') as HTMLButtonElement;
+ submitBtn.disabled = true;
+ submitBtn.textContent = 'Creating...';
+
+ const tagsStr = (overlay.querySelector('#task-tags') as HTMLInputElement).value;
+ const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(Boolean) : [];
+
+ try {
+ await api.createTask(projectName, {
+ title: (overlay.querySelector('#task-title') as HTMLInputElement).value,
+ description: (overlay.querySelector('#task-desc') as HTMLTextAreaElement).value || undefined,
+ priority: (overlay.querySelector('#task-priority') as HTMLSelectElement).value,
+ level: parseInt((overlay.querySelector('#task-level') as HTMLSelectElement).value),
+ tags,
+ });
+ overlay.remove();
+ await refreshBoard(boardRoot, projectName);
+ } catch (err) {
+ submitBtn.disabled = false;
+ submitBtn.textContent = 'Create Task';
+ alert((err as Error).message);
+ }
+ });
+
+ (overlay.querySelector('#task-title') as HTMLInputElement).focus();
+}
diff --git a/admin-panel/src/client/pages/settings.ts b/admin-panel/src/client/pages/settings.ts
new file mode 100644
index 0000000..e78c69f
--- /dev/null
+++ b/admin-panel/src/client/pages/settings.ts
@@ -0,0 +1,59 @@
+export async function renderSettings(container: HTMLElement): Promise<void> {
+ container.innerHTML = `
+ <div class="settings-container">
+ <h1 style="margin-bottom:24px">Settings</h1>
+
+ <div class="settings-section">
+ <h2>API Configuration</h2>
+ <div class="form-group">
+ <label for="api-key">Anthropic API Key</label>
+ <input type="password" id="api-key" placeholder="sk-ant-..." style="width:100%" />
+ <small style="color:var(--text-secondary);margin-top:4px;display:block">
+ Configured via ANTHROPIC_API_KEY environment variable.
+ The API key is set in the container's environment, not stored in the database.
+ </small>
+ </div>
+ </div>
+
+ <div class="settings-section">
+ <h2>Agent Models</h2>
+ <p style="color:var(--text-secondary);margin-bottom:16px;font-size:13px">
+ Configure which Claude model each agent uses.
+ </p>
+ <div class="form-group">
+ <label>Planner (Plan generation)</label>
+ <select disabled><option>Claude Opus</option></select>
+ </div>
+ <div class="form-group">
+ <label>Critic (Plan review)</label>
+ <select disabled><option>Claude Sonnet</option></select>
+ </div>
+ <div class="form-group">
+ <label>Builder (Implementation)</label>
+ <select disabled><option>Claude Opus</option></select>
+ </div>
+ <div class="form-group">
+ <label>Shield (Test writing)</label>
+ <select disabled><option>Claude Sonnet</option></select>
+ </div>
+ <div class="form-group">
+ <label>Inspector (Code review)</label>
+ <select disabled><option>Claude Sonnet</option></select>
+ </div>
+ <div class="form-group">
+ <label>Ranger (Test runner)</label>
+ <select disabled><option>Claude Sonnet</option></select>
+ </div>
+ <small style="color:var(--text-secondary)">Model configuration will be available when the AI pipeline is enabled.</small>
+ </div>
+
+ <div class="settings-section">
+ <h2>Git / SSH</h2>
+ <p style="color:var(--text-secondary);font-size:13px">
+ SSH key for git-server and Gerrit access is mounted at <code>/app/.ssh/</code> in the container.
+ Ensure the public key is added to <code>/var/git_ssh_keys/</code> on the host.
+ </p>
+ </div>
+ </div>
+ `;
+}
diff --git a/admin-panel/src/client/styles/main.css b/admin-panel/src/client/styles/main.css
new file mode 100644
index 0000000..d2fe35b
--- /dev/null
+++ b/admin-panel/src/client/styles/main.css
@@ -0,0 +1,724 @@
+/* Cyberpunk Dark Theme */
+:root {
+ --bg-deep: #0A0D1F;
+ --bg-card: #1A1A2E;
+ --accent: #7B2FBE;
+ --accent-glow: #A855F7;
+ --alert: #FF2D78;
+ --text-secondary: #8B9BB4;
+ --text-primary: #E8ECF4;
+ --bg-input: #12152a;
+ --border: #2a2a4a;
+ --success: #22c55e;
+ --warning: #eab308;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html, body {
+ height: 100%;
+ background: var(--bg-deep);
+ color: var(--text-primary);
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+#app {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+/* Navbar */
+#navbar {
+ display: flex;
+ align-items: center;
+ padding: 0 24px;
+ height: 52px;
+ background: var(--bg-card);
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+
+.nav-brand {
+ font-size: 18px;
+ font-weight: 700;
+ letter-spacing: 3px;
+ color: var(--accent-glow);
+ text-decoration: none;
+ margin-right: 32px;
+}
+
+.nav-links {
+ display: flex;
+ gap: 4px;
+}
+
+.nav-links a {
+ color: var(--text-secondary);
+ text-decoration: none;
+ padding: 6px 14px;
+ border-radius: 6px;
+ font-size: 13px;
+ transition: all 0.15s;
+}
+
+.nav-links a:hover,
+.nav-links a.active {
+ color: var(--text-primary);
+ background: rgba(123, 47, 190, 0.15);
+}
+
+.nav-links a.active {
+ color: var(--accent-glow);
+}
+
+/* Main content */
+#main-content {
+ flex: 1;
+ overflow: auto;
+ padding: 24px;
+}
+
+/* Scrollbar */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg-deep);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--text-secondary);
+}
+
+/* Buttons */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 16px;
+ border: none;
+ border-radius: 6px;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s;
+}
+
+.btn-primary {
+ background: var(--accent);
+ color: white;
+}
+
+.btn-primary:hover {
+ background: var(--accent-glow);
+ box-shadow: 0 0 20px rgba(168, 85, 247, 0.3);
+}
+
+.btn-secondary {
+ background: var(--border);
+ color: var(--text-primary);
+}
+
+.btn-secondary:hover {
+ background: var(--text-secondary);
+}
+
+.btn-danger {
+ background: transparent;
+ color: var(--alert);
+ border: 1px solid var(--alert);
+}
+
+.btn-danger:hover {
+ background: var(--alert);
+ color: white;
+}
+
+.btn-sm {
+ padding: 4px 10px;
+ font-size: 12px;
+}
+
+.btn-icon {
+ padding: 6px;
+ background: transparent;
+ color: var(--text-secondary);
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.btn-icon:hover {
+ color: var(--text-primary);
+ background: var(--border);
+}
+
+/* Form elements */
+input, textarea, select {
+ background: var(--bg-input);
+ border: 1px solid var(--border);
+ color: var(--text-primary);
+ padding: 8px 12px;
+ border-radius: 6px;
+ font-size: 13px;
+ font-family: inherit;
+ outline: none;
+ transition: border-color 0.15s;
+}
+
+input:focus, textarea:focus, select:focus {
+ border-color: var(--accent);
+}
+
+textarea {
+ resize: vertical;
+ min-height: 80px;
+}
+
+label {
+ display: block;
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--text-secondary);
+ margin-bottom: 4px;
+}
+
+.form-group {
+ margin-bottom: 16px;
+}
+
+.form-group input,
+.form-group textarea,
+.form-group select {
+ width: 100%;
+}
+
+/* Cards */
+.card {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 20px;
+}
+
+/* Modal */
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ animation: fadeIn 0.15s;
+}
+
+.modal {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ width: 90%;
+ max-width: 540px;
+ max-height: 90vh;
+ overflow-y: auto;
+ padding: 24px;
+ animation: slideUp 0.2s;
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 20px;
+}
+
+.modal-header h2 {
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.modal-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ margin-top: 20px;
+ padding-top: 16px;
+ border-top: 1px solid var(--border);
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes slideUp {
+ from { transform: translateY(20px); opacity: 0; }
+ to { transform: translateY(0); opacity: 1; }
+}
+
+/* Dashboard */
+.dashboard-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 24px;
+}
+
+.dashboard-header h1 {
+ font-size: 24px;
+ font-weight: 600;
+}
+
+.project-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ gap: 16px;
+}
+
+.project-card {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 20px;
+ cursor: pointer;
+ transition: all 0.15s;
+ text-decoration: none;
+ color: inherit;
+ display: block;
+}
+
+.project-card:hover {
+ border-color: var(--accent);
+ box-shadow: 0 0 20px rgba(123, 47, 190, 0.15);
+ transform: translateY(-1px);
+}
+
+.project-card h3 {
+ font-size: 16px;
+ font-weight: 600;
+ margin-bottom: 4px;
+}
+
+.project-card .project-name {
+ font-size: 12px;
+ color: var(--text-secondary);
+ margin-bottom: 8px;
+ font-family: monospace;
+}
+
+.project-card .project-desc {
+ color: var(--text-secondary);
+ font-size: 13px;
+ margin-bottom: 12px;
+}
+
+.project-card .project-stats {
+ display: flex;
+ gap: 12px;
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.project-card .stat {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.stat-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+}
+
+.stat-dot.todo { background: var(--text-secondary); }
+.stat-dot.active { background: var(--accent-glow); }
+.stat-dot.done { background: var(--success); }
+
+/* Kanban Board */
+.board-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 16px;
+}
+
+.board-header h1 {
+ font-size: 20px;
+ font-weight: 600;
+}
+
+.board-container {
+ display: flex;
+ gap: 12px;
+ overflow-x: auto;
+ height: calc(100vh - 140px);
+ padding-bottom: 16px;
+}
+
+.board-column {
+ flex: 0 0 260px;
+ display: flex;
+ flex-direction: column;
+ background: rgba(26, 26, 46, 0.3);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.column-header {
+ padding: 12px 14px;
+ background: var(--bg-card);
+ border-bottom: 2px solid var(--accent-glow);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-shrink: 0;
+}
+
+.column-header h3 {
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.column-count {
+ font-size: 11px;
+ color: var(--text-secondary);
+ background: var(--border);
+ padding: 1px 8px;
+ border-radius: 10px;
+}
+
+.column-body {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.column-body.drag-over {
+ background: rgba(123, 47, 190, 0.08);
+}
+
+/* Task Cards */
+.task-card {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-left: 3px solid var(--accent);
+ border-radius: 6px;
+ padding: 12px;
+ cursor: pointer;
+ transition: all 0.15s;
+ user-select: none;
+}
+
+.task-card:hover {
+ border-color: var(--accent);
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
+}
+
+.task-card.dragging {
+ opacity: 0.5;
+ transform: rotate(2deg);
+}
+
+.task-card.pipeline-active {
+ animation: pulseGlow 2s infinite;
+}
+
+@keyframes pulseGlow {
+ 0%, 100% { box-shadow: 0 0 5px rgba(168, 85, 247, 0.2); }
+ 50% { box-shadow: 0 0 20px rgba(168, 85, 247, 0.5); }
+}
+
+.task-card-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ margin-bottom: 6px;
+}
+
+.task-id {
+ font-size: 11px;
+ color: var(--text-secondary);
+ font-family: monospace;
+}
+
+.task-title {
+ font-size: 13px;
+ font-weight: 500;
+ line-height: 1.4;
+ margin-bottom: 8px;
+}
+
+.task-meta {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+
+.badge {
+ font-size: 10px;
+ font-weight: 600;
+ padding: 2px 8px;
+ border-radius: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.badge-high { background: var(--alert); color: white; }
+.badge-medium { background: var(--accent); color: white; }
+.badge-low { background: var(--border); color: var(--text-secondary); }
+
+.badge-level {
+ background: var(--border);
+ color: var(--text-secondary);
+}
+
+.badge-agent {
+ background: rgba(168, 85, 247, 0.2);
+ color: var(--accent-glow);
+}
+
+.tag {
+ font-size: 10px;
+ padding: 1px 6px;
+ border-radius: 3px;
+ background: var(--border);
+ color: var(--text-secondary);
+}
+
+/* Task Detail Modal */
+.task-modal {
+ max-width: 800px;
+}
+
+.task-modal .modal-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+}
+
+.task-modal .modal-header .top-row {
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.task-modal .tabs {
+ display: flex;
+ gap: 2px;
+ border-bottom: 1px solid var(--border);
+ margin-bottom: 16px;
+}
+
+.task-modal .tab {
+ padding: 8px 16px;
+ font-size: 13px;
+ color: var(--text-secondary);
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: all 0.15s;
+ background: none;
+ border-top: none;
+ border-left: none;
+ border-right: none;
+}
+
+.task-modal .tab:hover {
+ color: var(--text-primary);
+}
+
+.task-modal .tab.active {
+ color: var(--accent-glow);
+ border-bottom-color: var(--accent-glow);
+}
+
+.tab-content {
+ min-height: 200px;
+}
+
+.lifecycle-bar {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 16px;
+}
+
+.lifecycle-step {
+ flex: 1;
+ height: 4px;
+ border-radius: 2px;
+ background: var(--border);
+}
+
+.lifecycle-step.completed {
+ background: var(--accent-glow);
+}
+
+.lifecycle-step.current {
+ background: var(--accent-glow);
+ animation: pulseGlow 2s infinite;
+}
+
+/* Agent Log */
+.agent-log {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.log-entry {
+ display: flex;
+ gap: 12px;
+ padding: 12px;
+ background: var(--bg-deep);
+ border-radius: 6px;
+ font-size: 13px;
+}
+
+.log-entry .log-time {
+ color: var(--text-secondary);
+ font-family: monospace;
+ font-size: 11px;
+ white-space: nowrap;
+}
+
+.log-entry .log-agent {
+ color: var(--accent-glow);
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+.log-entry .log-message {
+ color: var(--text-secondary);
+}
+
+/* Diff Viewer */
+.diff-viewer {
+ font-family: 'Consolas', 'Monaco', monospace;
+ font-size: 12px;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ overflow: auto;
+ max-height: 500px;
+}
+
+.diff-line {
+ padding: 1px 12px;
+ white-space: pre;
+}
+
+.diff-add {
+ background: rgba(34, 197, 94, 0.1);
+ color: #4ade80;
+}
+
+.diff-del {
+ background: rgba(255, 45, 120, 0.1);
+ color: #ff6b9d;
+}
+
+.diff-hunk {
+ color: var(--accent-glow);
+ background: rgba(168, 85, 247, 0.05);
+}
+
+/* Pipeline Controls */
+.pipeline-controls {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+/* Settings Page */
+.settings-container {
+ max-width: 600px;
+}
+
+.settings-section {
+ margin-bottom: 32px;
+}
+
+.settings-section h2 {
+ font-size: 16px;
+ margin-bottom: 16px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid var(--border);
+}
+
+/* Empty state */
+.empty-state {
+ text-align: center;
+ padding: 48px 24px;
+ color: var(--text-secondary);
+}
+
+.empty-state h3 {
+ font-size: 16px;
+ margin-bottom: 8px;
+ color: var(--text-primary);
+}
+
+/* Toast notifications */
+.toast-container {
+ position: fixed;
+ bottom: 24px;
+ right: 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ z-index: 2000;
+}
+
+.toast {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 12px 16px;
+ font-size: 13px;
+ animation: slideUp 0.2s;
+ max-width: 360px;
+}
+
+.toast.error {
+ border-color: var(--alert);
+}
+
+.toast.success {
+ border-color: var(--success);
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .board-container {
+ gap: 8px;
+ }
+
+ .board-column {
+ flex: 0 0 220px;
+ }
+
+ .project-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/admin-panel/src/server/agents/base-agent.ts b/admin-panel/src/server/agents/base-agent.ts
new file mode 100644
index 0000000..f9e49f7
--- /dev/null
+++ b/admin-panel/src/server/agents/base-agent.ts
@@ -0,0 +1,83 @@
+import { readFileSync } from 'fs';
+import { join, dirname } from 'path';
+import { fileURLToPath } from 'url';
+import { callClaude, type ClaudeResponse } from '../services/claude.service.js';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const TEMPLATES_DIR = join(__dirname, '..', 'templates');
+
+export interface AgentContext {
+ taskId: number;
+ title: string;
+ description: string;
+ plan?: string;
+ decisionLog?: string;
+ doneWhen?: string;
+ implementationNotes?: string;
+ diff?: string;
+ testResults?: string;
+ reviewComments?: string;
+ branchName?: string;
+ repoPath?: string;
+ workspacePath?: string;
+ fileList?: string;
+ fileContents?: string;
+}
+
+export interface AgentResult {
+ success: boolean;
+ content: string;
+ tokensUsed: number;
+ model: string;
+ updates: Record<string, unknown>; // Fields to update on the task
+ verdict?: 'approve' | 'reject'; // For review agents
+ message: string; // Log message
+}
+
+export abstract class BaseAgent {
+ abstract name: string;
+ abstract templateFile: string;
+ abstract model: string;
+
+ protected loadTemplate(): string {
+ return readFileSync(join(TEMPLATES_DIR, this.templateFile), 'utf-8');
+ }
+
+ protected fillTemplate(template: string, ctx: AgentContext): string {
+ return template
+ .replace(/\{\{taskId\}\}/g, String(ctx.taskId))
+ .replace(/\{\{title\}\}/g, ctx.title || '')
+ .replace(/\{\{description\}\}/g, ctx.description || '')
+ .replace(/\{\{plan\}\}/g, ctx.plan || '')
+ .replace(/\{\{decision_log\}\}/g, ctx.decisionLog || '')
+ .replace(/\{\{done_when\}\}/g, ctx.doneWhen || '')
+ .replace(/\{\{implementation_notes\}\}/g, ctx.implementationNotes || '')
+ .replace(/\{\{diff\}\}/g, ctx.diff || '')
+ .replace(/\{\{test_results\}\}/g, ctx.testResults || '')
+ .replace(/\{\{review_comments\}\}/g, ctx.reviewComments || '')
+ .replace(/\{\{branch_name\}\}/g, ctx.branchName || '')
+ .replace(/\{\{file_list\}\}/g, ctx.fileList || '')
+ .replace(/\{\{file_contents\}\}/g, ctx.fileContents || '');
+ }
+
+ async execute(ctx: AgentContext): Promise<AgentResult> {
+ const template = this.loadTemplate();
+ const prompt = this.fillTemplate(template, ctx);
+
+ const response = await callClaude({
+ model: this.model,
+ systemPrompt: this.getSystemPrompt(),
+ userMessage: prompt,
+ maxTokens: this.getMaxTokens(),
+ });
+
+ return this.parseResponse(response, ctx);
+ }
+
+ protected abstract getSystemPrompt(): string;
+ protected abstract parseResponse(response: ClaudeResponse, ctx: AgentContext): AgentResult;
+
+ protected getMaxTokens(): number {
+ return 8192;
+ }
+}
diff --git a/admin-panel/src/server/agents/builder.ts b/admin-panel/src/server/agents/builder.ts
new file mode 100644
index 0000000..4e555be
--- /dev/null
+++ b/admin-panel/src/server/agents/builder.ts
@@ -0,0 +1,64 @@
+import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js';
+import type { ClaudeResponse } from '../services/claude.service.js';
+
+export class BuilderAgent extends BaseAgent {
+ name = 'builder';
+ templateFile = 'builder.md';
+ model = 'claude-opus-4-20250514';
+
+ protected getSystemPrompt(): string {
+ return 'You are an expert software engineer. Write clean, correct, production-quality code. Follow the plan precisely. Output complete file contents — never use placeholders or ellipses.';
+ }
+
+ protected getMaxTokens(): number {
+ return 16384;
+ }
+
+ protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult {
+ const content = response.content;
+
+ // Extract files from ```FILE: path``` blocks
+ const files = parseFileBlocks(content);
+ const summary = extractSection(content, 'Summary') || '';
+ const implNotes = extractSection(content, 'Implementation Notes') || '';
+
+ return {
+ success: true,
+ content,
+ tokensUsed: response.tokensUsed,
+ model: response.model,
+ updates: {
+ implementation_notes: implNotes || summary,
+ status: 'impl_review',
+ // files are handled by pipeline.service.ts which reads them from content
+ },
+ message: `Implemented ${files.length} file(s)`,
+ };
+ }
+}
+
+export interface FileChange {
+ path: string;
+ content: string;
+}
+
+export function parseFileBlocks(text: string): FileChange[] {
+ const files: FileChange[] = [];
+ const regex = /```FILE:\s*(.+?)\n([\s\S]*?)```/g;
+ let match;
+
+ while ((match = regex.exec(text)) !== null) {
+ files.push({
+ path: match[1].trim(),
+ content: match[2],
+ });
+ }
+
+ return files;
+}
+
+function extractSection(text: string, heading: string): string | null {
+ const regex = new RegExp(`###\\s*${heading}\\s*\\n([\\s\\S]*?)(?=###|$)`, 'i');
+ const match = text.match(regex);
+ return match ? match[1].trim() : null;
+}
diff --git a/admin-panel/src/server/agents/critic.ts b/admin-panel/src/server/agents/critic.ts
new file mode 100644
index 0000000..b0613fb
--- /dev/null
+++ b/admin-panel/src/server/agents/critic.ts
@@ -0,0 +1,55 @@
+import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js';
+import type { ClaudeResponse } from '../services/claude.service.js';
+
+export class CriticAgent extends BaseAgent {
+ name = 'critic';
+ templateFile = 'critic.md';
+ model = 'claude-sonnet-4-20250514';
+
+ protected getSystemPrompt(): string {
+ return 'You are a thorough plan reviewer. Be constructive but rigorous. Approve plans that are solid enough to implement, reject plans with significant gaps or flaws.';
+ }
+
+ protected parseResponse(response: ClaudeResponse, ctx: AgentContext): AgentResult {
+ const content = response.content;
+ const firstLine = content.split('\n')[0].trim().toUpperCase();
+ const approved = firstLine.includes('APPROVE');
+ const rejected = firstLine.includes('REJECT');
+ const verdict: 'approve' | 'reject' = approved ? 'approve' : 'reject';
+
+ // Build review entry
+ const reviewEntry = {
+ agent: 'critic',
+ verdict,
+ content,
+ timestamp: new Date().toISOString(),
+ };
+
+ const updates: Record<string, unknown> = {};
+ const currentReviews = ctx.reviewComments ? JSON.parse(ctx.reviewComments || '[]') : [];
+ currentReviews.push(reviewEntry);
+ updates.plan_review_comments = JSON.stringify(currentReviews);
+
+ if (approved) {
+ updates.status = 'impl';
+ updates.plan_review_count = (ctx as unknown as Record<string, number>).plan_review_count
+ ? (ctx as unknown as Record<string, number>).plan_review_count + 1
+ : 1;
+ } else if (rejected) {
+ updates.status = 'plan'; // Send back to planning
+ updates.plan_review_count = (ctx as unknown as Record<string, number>).plan_review_count
+ ? (ctx as unknown as Record<string, number>).plan_review_count + 1
+ : 1;
+ }
+
+ return {
+ success: true,
+ content,
+ tokensUsed: response.tokensUsed,
+ model: response.model,
+ updates,
+ verdict,
+ message: `Plan review: ${verdict.toUpperCase()}`,
+ };
+ }
+}
diff --git a/admin-panel/src/server/agents/inspector.ts b/admin-panel/src/server/agents/inspector.ts
new file mode 100644
index 0000000..575c34e
--- /dev/null
+++ b/admin-panel/src/server/agents/inspector.ts
@@ -0,0 +1,54 @@
+import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js';
+import type { ClaudeResponse } from '../services/claude.service.js';
+
+export class InspectorAgent extends BaseAgent {
+ name = 'inspector';
+ templateFile = 'inspector.md';
+ model = 'claude-sonnet-4-20250514';
+
+ protected getSystemPrompt(): string {
+ return 'You are a senior code reviewer. Be thorough but pragmatic. Focus on correctness and security. Approve code that is production-ready, reject code with significant issues.';
+ }
+
+ protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult {
+ const content = response.content;
+ const firstLine = content.split('\n')[0].trim().toUpperCase();
+ const approved = firstLine.includes('APPROVE');
+ const verdict: 'approve' | 'reject' = approved ? 'approve' : 'reject';
+
+ const reviewEntry = {
+ agent: 'inspector',
+ verdict,
+ content,
+ timestamp: new Date().toISOString(),
+ };
+
+ const updates: Record<string, unknown> = {};
+
+ // Parse existing review comments
+ let currentReviews: unknown[];
+ try {
+ currentReviews = JSON.parse(_ctx.reviewComments || '[]');
+ } catch {
+ currentReviews = [];
+ }
+ currentReviews.push(reviewEntry);
+ updates.review_comments = JSON.stringify(currentReviews);
+
+ if (approved) {
+ updates.status = 'test';
+ } else {
+ updates.status = 'impl'; // Send back to builder
+ }
+
+ return {
+ success: true,
+ content,
+ tokensUsed: response.tokensUsed,
+ model: response.model,
+ updates,
+ verdict,
+ message: `Code review: ${verdict.toUpperCase()}`,
+ };
+ }
+}
diff --git a/admin-panel/src/server/agents/planner.ts b/admin-panel/src/server/agents/planner.ts
new file mode 100644
index 0000000..eb004f9
--- /dev/null
+++ b/admin-panel/src/server/agents/planner.ts
@@ -0,0 +1,45 @@
+import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js';
+import type { ClaudeResponse } from '../services/claude.service.js';
+
+export class PlannerAgent extends BaseAgent {
+ name = 'planner';
+ templateFile = 'planner.md';
+ model = 'claude-opus-4-20250514';
+
+ protected getSystemPrompt(): string {
+ return 'You are a meticulous software architect who creates clear, actionable implementation plans. Be thorough but concise. Focus on practical steps, not theory.';
+ }
+
+ protected getMaxTokens(): number {
+ return 16384;
+ }
+
+ protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult {
+ const content = response.content;
+
+ // Extract sections
+ const plan = content;
+ const doneWhen = extractSection(content, 'Done When') || '';
+ const decisionLog = extractSection(content, 'Decisions') || '';
+
+ return {
+ success: true,
+ content,
+ tokensUsed: response.tokensUsed,
+ model: response.model,
+ updates: {
+ plan,
+ done_when: doneWhen,
+ decision_log: decisionLog,
+ status: 'plan_review',
+ },
+ message: 'Generated implementation plan',
+ };
+ }
+}
+
+function extractSection(text: string, heading: string): string | null {
+ const regex = new RegExp(`###\\s*${heading}\\s*\\n([\\s\\S]*?)(?=###|$)`, 'i');
+ const match = text.match(regex);
+ return match ? match[1].trim() : null;
+}
diff --git a/admin-panel/src/server/agents/ranger.ts b/admin-panel/src/server/agents/ranger.ts
new file mode 100644
index 0000000..f1f3918
--- /dev/null
+++ b/admin-panel/src/server/agents/ranger.ts
@@ -0,0 +1,54 @@
+import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js';
+import type { ClaudeResponse } from '../services/claude.service.js';
+
+export class RangerAgent extends BaseAgent {
+ name = 'ranger';
+ templateFile = 'ranger.md';
+ model = 'claude-sonnet-4-20250514';
+
+ protected getSystemPrompt(): string {
+ return 'You are a QA engineer making the final pass/fail decision. Be fair but thorough. Only pass tasks that genuinely meet their acceptance criteria.';
+ }
+
+ protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult {
+ const content = response.content;
+ const firstLine = content.split('\n')[0].trim().toUpperCase();
+ const passed = firstLine.includes('PASS');
+ const verdict: 'approve' | 'reject' = passed ? 'approve' : 'reject';
+
+ const testEntry = {
+ agent: 'ranger',
+ verdict: passed ? 'pass' : 'fail',
+ content,
+ timestamp: new Date().toISOString(),
+ };
+
+ const updates: Record<string, unknown> = {};
+
+ let currentResults: unknown[];
+ try {
+ currentResults = JSON.parse(_ctx.testResults || '[]');
+ } catch {
+ currentResults = [];
+ }
+ currentResults.push(testEntry);
+ updates.test_results = JSON.stringify(currentResults);
+
+ if (passed) {
+ updates.status = 'done';
+ updates.completed_at = new Date().toISOString();
+ } else {
+ updates.status = 'impl'; // Send back to builder
+ }
+
+ return {
+ success: true,
+ content,
+ tokensUsed: response.tokensUsed,
+ model: response.model,
+ updates,
+ verdict,
+ message: `Final verdict: ${passed ? 'PASS' : 'FAIL'}`,
+ };
+ }
+}
diff --git a/admin-panel/src/server/agents/shield.ts b/admin-panel/src/server/agents/shield.ts
new file mode 100644
index 0000000..2ce0bc1
--- /dev/null
+++ b/admin-panel/src/server/agents/shield.ts
@@ -0,0 +1,62 @@
+import { BaseAgent, type AgentContext, type AgentResult } from './base-agent.js';
+import type { ClaudeResponse } from '../services/claude.service.js';
+
+export class ShieldAgent extends BaseAgent {
+ name = 'shield';
+ templateFile = 'shield.md';
+ model = 'claude-sonnet-4-20250514';
+
+ protected getSystemPrompt(): string {
+ return 'You are a test engineer who writes thorough, practical tests. Focus on verifying requirements and catching regressions. Use appropriate testing frameworks.';
+ }
+
+ protected getMaxTokens(): number {
+ return 12288;
+ }
+
+ protected parseResponse(response: ClaudeResponse, _ctx: AgentContext): AgentResult {
+ const content = response.content;
+
+ // Extract test files
+ const files = parseTestFiles(content);
+ const strategy = extractSection(content, 'Test Strategy') || '';
+
+ return {
+ success: true,
+ content,
+ tokensUsed: response.tokensUsed,
+ model: response.model,
+ updates: {
+ // Test files are written by pipeline service
+ // Test results will be populated after running
+ },
+ message: `Generated ${files.length} test file(s)`,
+ };
+ }
+}
+
+interface TestFile {
+ path: string;
+ content: string;
+}
+
+function parseTestFiles(text: string): TestFile[] {
+ const files: TestFile[] = [];
+ const regex = /```FILE:\s*(.+?)\n([\s\S]*?)```/g;
+ let match;
+
+ while ((match = regex.exec(text)) !== null) {
+ files.push({
+ path: match[1].trim(),
+ content: match[2],
+ });
+ }
+
+ return files;
+}
+
+function extractSection(text: string, heading: string): string | null {
+ const regex = new RegExp(`###\\s*${heading}\\s*\\n([\\s\\S]*?)(?=###|$)`, 'i');
+ const match = text.match(regex);
+ return match ? match[1].trim() : null;
+}
diff --git a/admin-panel/src/server/db.ts b/admin-panel/src/server/db.ts
new file mode 100644
index 0000000..9c83031
--- /dev/null
+++ b/admin-panel/src/server/db.ts
@@ -0,0 +1,82 @@
+import Database from 'better-sqlite3';
+import { readFileSync } from 'fs';
+import { join, dirname } from 'path';
+import { mkdirSync, existsSync } from 'fs';
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const DATA_DIR = join(process.cwd(), 'data');
+const SCHEMA_PATH = join(__dirname, 'schema.sql');
+
+const dbCache = new Map<string, Database.Database>();
+
+function ensureDataDir(): void {
+ if (!existsSync(DATA_DIR)) {
+ mkdirSync(DATA_DIR, { recursive: true });
+ }
+}
+
+function loadSchema(): { globalSchema: string; projectSchema: string } {
+ const full = readFileSync(SCHEMA_PATH, 'utf-8');
+ const marker = '-- === PROJECT_SCHEMA ===';
+ const idx = full.indexOf(marker);
+ if (idx === -1) {
+ return { globalSchema: full, projectSchema: '' };
+ }
+ return {
+ globalSchema: full.substring(0, idx).trim(),
+ projectSchema: full.substring(idx + marker.length).trim(),
+ };
+}
+
+export function getAdminDb(): Database.Database {
+ if (dbCache.has('admin')) {
+ return dbCache.get('admin')!;
+ }
+ ensureDataDir();
+ const db = new Database(join(DATA_DIR, 'admin.db'));
+ db.pragma('journal_mode = WAL');
+ db.pragma('foreign_keys = ON');
+
+ const { globalSchema } = loadSchema();
+ db.exec(globalSchema);
+
+ dbCache.set('admin', db);
+ return db;
+}
+
+export function getProjectDb(projectName: string): Database.Database {
+ if (dbCache.has(projectName)) {
+ return dbCache.get(projectName)!;
+ }
+ ensureDataDir();
+ const db = new Database(join(DATA_DIR, `${projectName}.db`));
+ db.pragma('journal_mode = WAL');
+ db.pragma('foreign_keys = ON');
+
+ const { projectSchema } = loadSchema();
+ db.exec(projectSchema);
+
+ dbCache.set(projectName, db);
+ return db;
+}
+
+export function closeAll(): void {
+ for (const [name, db] of dbCache) {
+ db.close();
+ dbCache.delete(name);
+ }
+}
+
+export function getSetting(key: string): string | undefined {
+ const db = getAdminDb();
+ const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as
+ | { value: string }
+ | undefined;
+ return row?.value;
+}
+
+export function setSetting(key: string, value: string): void {
+ const db = getAdminDb();
+ db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)').run(key, value);
+}
diff --git a/admin-panel/src/server/index.ts b/admin-panel/src/server/index.ts
new file mode 100644
index 0000000..f76602e
--- /dev/null
+++ b/admin-panel/src/server/index.ts
@@ -0,0 +1,46 @@
+import express from 'express';
+import { createServer } from 'http';
+import { join, dirname } from 'path';
+import { existsSync } from 'fs';
+import { fileURLToPath } from 'url';
+import { setupWebSocket } from './ws.js';
+import projectsRouter from './routes/projects.js';
+import tasksRouter from './routes/tasks.js';
+import pipelineRouter from './routes/pipeline.js';
+import gerritRouter from './routes/gerrit.js';
+import dashboardRouter from './routes/dashboard.js';
+import settingsRouter from './routes/settings.js';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const app = express();
+const server = createServer(app);
+const PORT = process.env.PORT || 3000;
+
+// Middleware
+app.use(express.json());
+
+// API routes
+app.use('/api/projects', projectsRouter);
+app.use('/api/projects/:project/tasks', tasksRouter);
+app.use('/api/projects/:project/pipeline', pipelineRouter);
+app.use('/api/projects/:project/gerrit', gerritRouter);
+app.use('/api/dashboard', dashboardRouter);
+app.use('/api/settings', settingsRouter);
+
+// Serve static client in production
+const clientDir = join(__dirname, '..', 'client');
+if (existsSync(clientDir)) {
+ app.use(express.static(clientDir));
+ app.get('*', (_req, res) => {
+ res.sendFile(join(clientDir, 'index.html'));
+ });
+}
+
+// WebSocket
+setupWebSocket(server);
+
+server.listen(PORT, () => {
+ console.log(`Admin panel running on port ${PORT}`);
+});
+
+export { app, server };
diff --git a/admin-panel/src/server/routes/dashboard.ts b/admin-panel/src/server/routes/dashboard.ts
new file mode 100644
index 0000000..3142e8e
--- /dev/null
+++ b/admin-panel/src/server/routes/dashboard.ts
@@ -0,0 +1,138 @@
+import { Router, Request, Response } from 'express';
+import { getAdminDb, getProjectDb } from '../db.js';
+
+const router = Router();
+
+interface Project {
+ id: number;
+ name: string;
+ display_name: string;
+}
+
+// GET /api/dashboard/stats
+router.get('/stats', (_req: Request, res: Response) => {
+ const adminDb = getAdminDb();
+ const projects = adminDb.prepare('SELECT * FROM projects').all() as Project[];
+
+ let totalTasks = 0;
+ let completedToday = 0;
+ const tasksByStatus: Record<string, number> = {};
+
+ const today = new Date().toISOString().split('T')[0];
+
+ for (const project of projects) {
+ try {
+ const db = getProjectDb(project.name);
+ const tasks = db.prepare('SELECT status, completed_at FROM tasks').all() as Array<{
+ status: string;
+ completed_at: string | null;
+ }>;
+
+ totalTasks += tasks.length;
+
+ for (const task of tasks) {
+ tasksByStatus[task.status] = (tasksByStatus[task.status] || 0) + 1;
+ if (task.completed_at && task.completed_at.startsWith(today)) {
+ completedToday++;
+ }
+ }
+ } catch {
+ // Project DB might not exist yet
+ }
+ }
+
+ res.json({
+ totalProjects: projects.length,
+ totalTasks,
+ tasksByStatus,
+ completedToday,
+ });
+});
+
+// GET /api/dashboard/activity
+router.get('/activity', (_req: Request, res: Response) => {
+ const adminDb = getAdminDb();
+ const projects = adminDb.prepare('SELECT * FROM projects').all() as Project[];
+
+ const activity: Array<{
+ project: string;
+ taskId: number;
+ taskTitle: string;
+ agent: string;
+ action: string;
+ timestamp: string;
+ }> = [];
+
+ for (const project of projects) {
+ try {
+ const db = getProjectDb(project.name);
+ const tasks = db.prepare('SELECT id, title, agent_log FROM tasks WHERE agent_log != \'[]\'').all() as Array<{
+ id: number;
+ title: string;
+ agent_log: string;
+ }>;
+
+ for (const task of tasks) {
+ try {
+ const logs = JSON.parse(task.agent_log) as Array<{
+ agent: string;
+ message: string;
+ timestamp: string;
+ }>;
+ for (const log of logs) {
+ activity.push({
+ project: project.name,
+ taskId: task.id,
+ taskTitle: task.title,
+ agent: log.agent,
+ action: log.message,
+ timestamp: log.timestamp,
+ });
+ }
+ } catch {
+ // Skip malformed logs
+ }
+ }
+ } catch {
+ // Project DB might not exist
+ }
+ }
+
+ // Sort by timestamp descending, take 20 most recent
+ activity.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
+ res.json(activity.slice(0, 20));
+});
+
+// GET /api/projects/:project/stats
+router.get('/projects/:project', (req: Request, res: Response) => {
+ const { project } = req.params;
+
+ const adminDb = getAdminDb();
+ if (!adminDb.prepare('SELECT id FROM projects WHERE name = ?').get(project)) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ try {
+ const db = getProjectDb(project as string);
+ const tasks = db.prepare('SELECT status FROM tasks').all() as Array<{ status: string }>;
+ const tasksByStatus: Record<string, number> = {};
+ for (const task of tasks) {
+ tasksByStatus[task.status] = (tasksByStatus[task.status] || 0) + 1;
+ }
+
+ const totalJobs = db.prepare('SELECT COUNT(*) as count FROM pipeline_jobs').get() as { count: number };
+ const totalTokens = db.prepare('SELECT SUM(tokens_used) as total FROM pipeline_jobs').get() as { total: number | null };
+
+ res.json({
+ totalTasks: tasks.length,
+ tasksByStatus,
+ totalPipelineJobs: totalJobs.count,
+ totalTokensUsed: totalTokens.total || 0,
+ });
+ } catch {
+ res.json({ totalTasks: 0, tasksByStatus: {}, totalPipelineJobs: 0, totalTokensUsed: 0 });
+ }
+});
+
+export default router;
diff --git a/admin-panel/src/server/routes/gerrit.ts b/admin-panel/src/server/routes/gerrit.ts
new file mode 100644
index 0000000..b48ea53
--- /dev/null
+++ b/admin-panel/src/server/routes/gerrit.ts
@@ -0,0 +1,68 @@
+import { Router, Request, Response } from 'express';
+import { getAdminDb, getProjectDb } from '../db.js';
+import { getChange, getChangeByNumber } from '../services/gerrit.service.js';
+
+const router = Router({ mergeParams: true });
+
+type Params = { project: string; taskId: string };
+
+// GET /api/projects/:project/gerrit/:taskId
+router.get('/:taskId', async (req: Request<Params>, res: Response) => {
+ const { project, taskId } = req.params;
+
+ const adminDb = getAdminDb();
+ if (!adminDb.prepare('SELECT id FROM projects WHERE name = ?').get(project)) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ const db = getProjectDb(project);
+ const task = db.prepare('SELECT gerrit_change_id, gerrit_change_number FROM tasks WHERE id = ?').get(parseInt(taskId)) as {
+ gerrit_change_id: string | null;
+ gerrit_change_number: number | null;
+ } | undefined;
+
+ if (!task) {
+ res.status(404).json({ error: 'Task not found' });
+ return;
+ }
+
+ if (!task.gerrit_change_id && !task.gerrit_change_number) {
+ res.json({ change: null });
+ return;
+ }
+
+ try {
+ const change = task.gerrit_change_id
+ ? await getChange(task.gerrit_change_id)
+ : task.gerrit_change_number
+ ? await getChangeByNumber(task.gerrit_change_number)
+ : null;
+ res.json({ change });
+ } catch (err) {
+ res.status(502).json({ error: `Gerrit API error: ${(err as Error).message}` });
+ }
+});
+
+// GET /api/projects/:project/diff/:taskId
+router.get('/diff/:taskId', (req: Request<Params>, res: Response) => {
+ const { project, taskId } = req.params;
+
+ const adminDb = getAdminDb();
+ if (!adminDb.prepare('SELECT id FROM projects WHERE name = ?').get(project)) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ const db = getProjectDb(project);
+ const task = db.prepare('SELECT diff FROM tasks WHERE id = ?').get(parseInt(taskId)) as { diff: string | null } | undefined;
+
+ if (!task) {
+ res.status(404).json({ error: 'Task not found' });
+ return;
+ }
+
+ res.json({ diff: task.diff || '' });
+});
+
+export default router;
diff --git a/admin-panel/src/server/routes/pipeline.ts b/admin-panel/src/server/routes/pipeline.ts
new file mode 100644
index 0000000..39df35c
--- /dev/null
+++ b/admin-panel/src/server/routes/pipeline.ts
@@ -0,0 +1,82 @@
+import { Router, Request, Response } from 'express';
+import { getAdminDb } from '../db.js';
+import {
+ runFullPipeline,
+ runStep,
+ stopPipeline,
+ getPipelineStatus,
+ isPipelineRunning,
+} from '../services/pipeline.service.js';
+
+const router = Router({ mergeParams: true });
+
+type Params = { project: string; taskId: string };
+
+function projectExists(name: string): boolean {
+ const db = getAdminDb();
+ return !!db.prepare('SELECT id FROM projects WHERE name = ?').get(name);
+}
+
+// POST /api/projects/:project/pipeline/run/:taskId
+router.post('/run/:taskId', async (req: Request<Params>, res: Response) => {
+ const { project, taskId } = req.params;
+ if (!projectExists(project)) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ const id = parseInt(taskId);
+ if (isPipelineRunning(project, id)) {
+ res.status(409).json({ error: 'Pipeline already running for this task' });
+ return;
+ }
+
+ // Run async — don't block the response
+ runFullPipeline(project, id).catch(err => {
+ console.error(`Pipeline error for ${project}/${taskId}:`, err);
+ });
+
+ res.json({ status: 'started' });
+});
+
+// POST /api/projects/:project/pipeline/step/:taskId
+router.post('/step/:taskId', async (req: Request<Params>, res: Response) => {
+ const { project, taskId } = req.params;
+ if (!projectExists(project)) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ try {
+ await runStep(project, parseInt(taskId));
+ res.json({ status: 'completed' });
+ } catch (err) {
+ res.status(400).json({ error: (err as Error).message });
+ }
+});
+
+// POST /api/projects/:project/pipeline/stop/:taskId
+router.post('/stop/:taskId', (req: Request<Params>, res: Response) => {
+ const { project, taskId } = req.params;
+ if (!projectExists(project)) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ stopPipeline(project, parseInt(taskId));
+ res.json({ status: 'stopping' });
+});
+
+// GET /api/projects/:project/pipeline/status/:taskId
+router.get('/status/:taskId', (req: Request<Params>, res: Response) => {
+ const { project, taskId } = req.params;
+ if (!projectExists(project)) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ const status = getPipelineStatus(project, parseInt(taskId));
+ res.json(status);
+});
+
+export default router;
diff --git a/admin-panel/src/server/routes/projects.ts b/admin-panel/src/server/routes/projects.ts
new file mode 100644
index 0000000..b2e63a8
--- /dev/null
+++ b/admin-panel/src/server/routes/projects.ts
@@ -0,0 +1,138 @@
+import { Router, Request, Response } from 'express';
+import { getAdminDb, getProjectDb } from '../db.js';
+import { createBareRepo, setRepoDescription, repoExists, deleteRepo } from '../services/git.service.js';
+
+const router = Router();
+
+interface Project {
+ id: number;
+ name: string;
+ display_name: string;
+ description: string | null;
+ repo_name: string;
+ repo_path: string;
+ default_branch: string;
+ created_at: string;
+ updated_at: string;
+}
+
+// GET /api/projects
+router.get('/', (_req: Request, res: Response) => {
+ const db = getAdminDb();
+ const projects = db.prepare('SELECT * FROM projects ORDER BY created_at DESC').all();
+ res.json(projects);
+});
+
+// POST /api/projects
+router.post('/', (req: Request, res: Response) => {
+ const { name, display_name, description } = req.body;
+
+ if (!name || !display_name) {
+ res.status(400).json({ error: 'name and display_name are required' });
+ return;
+ }
+
+ // Sanitize name: lowercase, alphanumeric + hyphens
+ const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
+ if (!safeName) {
+ res.status(400).json({ error: 'Invalid project name' });
+ return;
+ }
+
+ const repoName = `${safeName}.git`;
+ const db = getAdminDb();
+
+ // Check for duplicate
+ const existing = db.prepare('SELECT id FROM projects WHERE name = ?').get(safeName);
+ if (existing) {
+ res.status(409).json({ error: 'Project already exists' });
+ return;
+ }
+
+ // Create bare git repo
+ const repo = createBareRepo(repoName);
+ if (description) {
+ setRepoDescription(repoName, description);
+ }
+
+ // Insert into admin DB
+ const result = db.prepare(
+ 'INSERT INTO projects (name, display_name, description, repo_name, repo_path) VALUES (?, ?, ?, ?, ?)'
+ ).run(safeName, display_name, description || null, repoName, repo.path);
+
+ // Initialize project DB (creates tables)
+ getProjectDb(safeName);
+
+ const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(result.lastInsertRowid);
+ res.status(201).json(project);
+});
+
+// GET /api/projects/:name
+router.get('/:name', (req: Request, res: Response) => {
+ const db = getAdminDb();
+ const project = db.prepare('SELECT * FROM projects WHERE name = ?').get(req.params.name) as Project | undefined;
+ if (!project) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+ res.json(project);
+});
+
+// PATCH /api/projects/:name
+router.patch('/:name', (req: Request, res: Response) => {
+ const db = getAdminDb();
+ const project = db.prepare('SELECT * FROM projects WHERE name = ?').get(req.params.name) as Project | undefined;
+ if (!project) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ const { display_name, description } = req.body;
+ const updates: string[] = [];
+ const params: unknown[] = [];
+
+ if (display_name !== undefined) {
+ updates.push('display_name = ?');
+ params.push(display_name);
+ }
+ if (description !== undefined) {
+ updates.push('description = ?');
+ params.push(description);
+ if (description) {
+ setRepoDescription(project.repo_name, description);
+ }
+ }
+
+ if (updates.length === 0) {
+ res.json(project);
+ return;
+ }
+
+ updates.push("updated_at = datetime('now')");
+ params.push(req.params.name);
+
+ db.prepare(`UPDATE projects SET ${updates.join(', ')} WHERE name = ?`).run(...params);
+ const updated = db.prepare('SELECT * FROM projects WHERE name = ?').get(req.params.name);
+ res.json(updated);
+});
+
+// DELETE /api/projects/:name
+router.delete('/:name', (req: Request, res: Response) => {
+ const db = getAdminDb();
+ const project = db.prepare('SELECT * FROM projects WHERE name = ?').get(req.params.name) as Project | undefined;
+ if (!project) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ db.prepare('DELETE FROM projects WHERE name = ?').run(req.params.name);
+
+ // Optionally delete repo (controlled by query param)
+ if (req.query.delete_repo === 'true') {
+ deleteRepo(project.repo_name);
+ }
+
+ res.status(204).send();
+});
+
+export default router;
diff --git a/admin-panel/src/server/routes/settings.ts b/admin-panel/src/server/routes/settings.ts
new file mode 100644
index 0000000..4eed6f1
--- /dev/null
+++ b/admin-panel/src/server/routes/settings.ts
@@ -0,0 +1,35 @@
+import { Router, Request, Response } from 'express';
+import { getAdminDb } from '../db.js';
+
+const router = Router();
+
+// GET /api/settings
+router.get('/', (_req: Request, res: Response) => {
+ const db = getAdminDb();
+ const rows = db.prepare('SELECT key, value FROM settings').all() as Array<{
+ key: string;
+ value: string;
+ }>;
+ const settings: Record<string, string> = {};
+ for (const row of rows) {
+ settings[row.key] = row.value;
+ }
+ res.json(settings);
+});
+
+// PATCH /api/settings
+router.patch('/', (req: Request, res: Response) => {
+ const db = getAdminDb();
+ const stmt = db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)');
+
+ const updates = req.body as Record<string, string>;
+ for (const [key, value] of Object.entries(updates)) {
+ if (typeof key === 'string' && typeof value === 'string') {
+ stmt.run(key, value);
+ }
+ }
+
+ res.status(204).send();
+});
+
+export default router;
diff --git a/admin-panel/src/server/routes/tasks.ts b/admin-panel/src/server/routes/tasks.ts
new file mode 100644
index 0000000..1ab1e8d
--- /dev/null
+++ b/admin-panel/src/server/routes/tasks.ts
@@ -0,0 +1,248 @@
+import { Router, Request, Response } from 'express';
+import { getAdminDb, getProjectDb } from '../db.js';
+import { broadcast } from '../ws.js';
+
+const router = Router({ mergeParams: true });
+
+type Params = { project: string; id?: string };
+
+interface Task {
+ id: number;
+ title: string;
+ status: string;
+ priority: string;
+ level: number;
+ description: string | null;
+ plan: string | null;
+ decision_log: string | null;
+ done_when: string | null;
+ implementation_notes: string | null;
+ tags: string;
+ plan_review_comments: string;
+ review_comments: string;
+ test_results: string;
+ agent_log: string;
+ current_agent: string | null;
+ plan_review_count: number;
+ impl_review_count: number;
+ branch_name: string | null;
+ gerrit_change_id: string | null;
+ gerrit_change_number: number | null;
+ diff: string | null;
+ rank: number;
+ created_at: string;
+ started_at: string | null;
+ planned_at: string | null;
+ reviewed_at: string | null;
+ tested_at: string | null;
+ completed_at: string | null;
+}
+
+const VALID_STATUSES = ['todo', 'plan', 'plan_review', 'impl', 'impl_review', 'test', 'done'];
+const VALID_PRIORITIES = ['high', 'medium', 'low'];
+
+function getProject(projectName: string): boolean {
+ const db = getAdminDb();
+ return !!db.prepare('SELECT id FROM projects WHERE name = ?').get(projectName);
+}
+
+// GET /api/projects/:project/tasks
+router.get('/', (req: Request<Params>, res: Response) => {
+ const { project } = req.params;
+ if (!getProject(project)) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ const db = getProjectDb(project);
+ const tasks = db.prepare('SELECT * FROM tasks ORDER BY rank ASC, id ASC').all();
+ res.json(tasks);
+});
+
+// POST /api/projects/:project/tasks
+router.post('/', (req: Request<Params>, res: Response) => {
+ const { project } = req.params;
+ if (!getProject(project)) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ const { title, description, priority, level, tags, status } = req.body;
+ if (!title) {
+ res.status(400).json({ error: 'title is required' });
+ return;
+ }
+
+ const db = getProjectDb(project);
+
+ // Get max rank for ordering
+ const maxRank = (db.prepare('SELECT MAX(rank) as max FROM tasks').get() as { max: number | null })?.max || 0;
+
+ const result = db.prepare(
+ `INSERT INTO tasks (title, description, priority, level, tags, status, rank)
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
+ ).run(
+ title,
+ description || null,
+ VALID_PRIORITIES.includes(priority) ? priority : 'medium',
+ [1, 2, 3].includes(level) ? level : 3,
+ JSON.stringify(tags || []),
+ VALID_STATUSES.includes(status) ? status : 'todo',
+ maxRank + 1
+ );
+
+ const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(result.lastInsertRowid);
+ broadcast(project, { type: 'task:created', project, task });
+ res.status(201).json(task);
+});
+
+// GET /api/projects/:project/tasks/:id
+router.get('/:id', (req: Request<Params>, res: Response) => {
+ const { project, id } = req.params;
+ if (!getProject(project)) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ const db = getProjectDb(project);
+ const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
+ if (!task) {
+ res.status(404).json({ error: 'Task not found' });
+ return;
+ }
+ res.json(task);
+});
+
+// PATCH /api/projects/:project/tasks/:id
+router.patch('/:id', (req: Request<Params>, res: Response) => {
+ const { project, id } = req.params;
+ if (!getProject(project)) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ const db = getProjectDb(project);
+ const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id) as Task | undefined;
+ if (!existing) {
+ res.status(404).json({ error: 'Task not found' });
+ return;
+ }
+
+ const allowedFields = [
+ 'title', 'status', 'priority', 'level', 'description', 'plan',
+ 'decision_log', 'done_when', 'implementation_notes', 'tags',
+ 'plan_review_comments', 'review_comments', 'test_results',
+ 'agent_log', 'current_agent', 'plan_review_count', 'impl_review_count',
+ 'branch_name', 'gerrit_change_id', 'gerrit_change_number', 'diff', 'rank'
+ ];
+
+ const updates: string[] = [];
+ const params: unknown[] = [];
+
+ for (const field of allowedFields) {
+ if (req.body[field] !== undefined) {
+ let value = req.body[field];
+
+ // Validate specific fields
+ if (field === 'status' && !VALID_STATUSES.includes(value)) continue;
+ if (field === 'priority' && !VALID_PRIORITIES.includes(value)) continue;
+ if (field === 'level' && ![1, 2, 3].includes(value)) continue;
+
+ // Stringify JSON fields
+ if (['tags', 'plan_review_comments', 'review_comments', 'test_results', 'agent_log'].includes(field)) {
+ value = typeof value === 'string' ? value : JSON.stringify(value);
+ }
+
+ updates.push(`${field} = ?`);
+ params.push(value);
+ }
+ }
+
+ // Set timestamp fields based on status changes
+ if (req.body.status) {
+ const now = new Date().toISOString();
+ switch (req.body.status) {
+ case 'plan':
+ if (!existing.started_at) {
+ updates.push('started_at = ?');
+ params.push(now);
+ }
+ break;
+ case 'impl':
+ updates.push('planned_at = ?');
+ params.push(now);
+ break;
+ case 'test':
+ updates.push('reviewed_at = ?');
+ params.push(now);
+ break;
+ case 'done':
+ updates.push('completed_at = ?');
+ params.push(now);
+ break;
+ }
+ }
+
+ if (updates.length === 0) {
+ res.json(existing);
+ return;
+ }
+
+ params.push(id);
+ db.prepare(`UPDATE tasks SET ${updates.join(', ')} WHERE id = ?`).run(...params);
+ const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
+ broadcast(project, { type: 'task:updated', project, task });
+ res.json(task);
+});
+
+// DELETE /api/projects/:project/tasks/:id
+router.delete('/:id', (req: Request<Params>, res: Response) => {
+ const { project, id } = req.params;
+ if (!getProject(project)) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ const db = getProjectDb(project);
+ const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
+ if (!task) {
+ res.status(404).json({ error: 'Task not found' });
+ return;
+ }
+
+ db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
+ broadcast(project, { type: 'task:deleted', project, taskId: Number(id) });
+ res.status(204).send();
+});
+
+// PATCH /api/projects/:project/tasks/:id/reorder
+router.patch('/:id/reorder', (req: Request<Params>, res: Response) => {
+ const { project, id } = req.params;
+ if (!getProject(project)) {
+ res.status(404).json({ error: 'Project not found' });
+ return;
+ }
+
+ const { rank, status } = req.body;
+ if (rank === undefined) {
+ res.status(400).json({ error: 'rank is required' });
+ return;
+ }
+
+ const db = getProjectDb(project);
+ const updates: string[] = ['rank = ?'];
+ const params: unknown[] = [rank];
+
+ if (status && VALID_STATUSES.includes(status)) {
+ updates.push('status = ?');
+ params.push(status);
+ }
+
+ params.push(id);
+ db.prepare(`UPDATE tasks SET ${updates.join(', ')} WHERE id = ?`).run(...params);
+ const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
+ broadcast(project, { type: 'task:updated', project, task });
+ res.json(task);
+});
+
+export default router;
diff --git a/admin-panel/src/server/schema.sql b/admin-panel/src/server/schema.sql
new file mode 100644
index 0000000..36c0e40
--- /dev/null
+++ b/admin-panel/src/server/schema.sql
@@ -0,0 +1,73 @@
+-- Global database schema (admin.db)
+
+CREATE TABLE IF NOT EXISTS projects (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL UNIQUE,
+ display_name TEXT NOT NULL,
+ description TEXT,
+ repo_name TEXT NOT NULL,
+ repo_path TEXT NOT NULL,
+ default_branch TEXT DEFAULT 'main',
+ created_at TEXT DEFAULT (datetime('now')),
+ updated_at TEXT DEFAULT (datetime('now'))
+);
+
+CREATE TABLE IF NOT EXISTS settings (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL
+);
+
+-- Default settings
+INSERT OR IGNORE INTO settings (key, value) VALUES
+ ('anthropic_model_planning', 'claude-opus-4-20250514'),
+ ('anthropic_model_review', 'claude-sonnet-4-20250514'),
+ ('anthropic_model_implementation', 'claude-opus-4-20250514'),
+ ('anthropic_model_testing', 'claude-sonnet-4-20250514');
+
+-- Per-project database schema (applied to {project-name}.db)
+-- This comment marks where project schema starts; code splits on this marker.
+-- === PROJECT_SCHEMA ===
+
+CREATE TABLE IF NOT EXISTS tasks (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ title TEXT NOT NULL,
+ status TEXT NOT NULL DEFAULT 'todo',
+ priority TEXT NOT NULL DEFAULT 'medium',
+ level INTEGER NOT NULL DEFAULT 3,
+ description TEXT,
+ plan TEXT,
+ decision_log TEXT,
+ done_when TEXT,
+ implementation_notes TEXT,
+ tags TEXT DEFAULT '[]',
+ plan_review_comments TEXT DEFAULT '[]',
+ review_comments TEXT DEFAULT '[]',
+ test_results TEXT DEFAULT '[]',
+ agent_log TEXT DEFAULT '[]',
+ current_agent TEXT,
+ plan_review_count INTEGER DEFAULT 0,
+ impl_review_count INTEGER DEFAULT 0,
+ branch_name TEXT,
+ gerrit_change_id TEXT,
+ gerrit_change_number INTEGER,
+ diff TEXT,
+ rank REAL DEFAULT 0,
+ created_at TEXT DEFAULT (datetime('now')),
+ started_at TEXT,
+ planned_at TEXT,
+ reviewed_at TEXT,
+ tested_at TEXT,
+ completed_at TEXT
+);
+
+CREATE TABLE IF NOT EXISTS pipeline_jobs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ task_id INTEGER NOT NULL REFERENCES tasks(id),
+ agent TEXT NOT NULL,
+ status TEXT NOT NULL DEFAULT 'pending',
+ model TEXT NOT NULL,
+ started_at TEXT,
+ completed_at TEXT,
+ error TEXT,
+ tokens_used INTEGER DEFAULT 0
+);
diff --git a/admin-panel/src/server/services/claude.service.ts b/admin-panel/src/server/services/claude.service.ts
new file mode 100644
index 0000000..b9aae37
--- /dev/null
+++ b/admin-panel/src/server/services/claude.service.ts
@@ -0,0 +1,47 @@
+import Anthropic from '@anthropic-ai/sdk';
+
+let client: Anthropic | null = null;
+
+function getClient(): Anthropic {
+ if (!client) {
+ const apiKey = process.env.ANTHROPIC_API_KEY;
+ if (!apiKey) throw new Error('ANTHROPIC_API_KEY not configured');
+ client = new Anthropic({ apiKey });
+ }
+ return client;
+}
+
+export interface ClaudeResponse {
+ content: string;
+ tokensUsed: number;
+ model: string;
+}
+
+export async function callClaude(opts: {
+ model: string;
+ systemPrompt: string;
+ userMessage: string;
+ maxTokens?: number;
+}): Promise<ClaudeResponse> {
+ const anthropic = getClient();
+
+ const response = await anthropic.messages.create({
+ model: opts.model,
+ max_tokens: opts.maxTokens || 8192,
+ system: opts.systemPrompt,
+ messages: [{ role: 'user', content: opts.userMessage }],
+ });
+
+ const content = response.content
+ .filter(block => block.type === 'text')
+ .map(block => (block as { type: 'text'; text: string }).text)
+ .join('\n');
+
+ const tokensUsed = (response.usage?.input_tokens || 0) + (response.usage?.output_tokens || 0);
+
+ return {
+ content,
+ tokensUsed,
+ model: opts.model,
+ };
+}
diff --git a/admin-panel/src/server/services/gerrit.service.ts b/admin-panel/src/server/services/gerrit.service.ts
new file mode 100644
index 0000000..53c8f13
--- /dev/null
+++ b/admin-panel/src/server/services/gerrit.service.ts
@@ -0,0 +1,81 @@
+const GERRIT_URL = process.env.GERRIT_URL || 'http://gerrit:8080';
+
+export interface GerritChange {
+ id: string;
+ change_id: string;
+ _number: number;
+ subject: string;
+ status: string;
+ created: string;
+ updated: string;
+ mergeable?: boolean;
+ labels?: Record<string, unknown>;
+}
+
+export interface GerritReview {
+ message: string;
+ labels?: Record<string, number>;
+ comments?: Record<string, Array<{ line: number; message: string }>>;
+}
+
+async function gerritFetch(path: string, options?: RequestInit): Promise<unknown> {
+ const url = `${GERRIT_URL}/a${path}`;
+ const res = await fetch(url, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options?.headers,
+ },
+ ...options,
+ });
+
+ if (!res.ok) {
+ throw new Error(`Gerrit API error: ${res.status} ${res.statusText}`);
+ }
+
+ const text = await res.text();
+ // Gerrit prepends )]}' to JSON responses
+ const json = text.startsWith(")]}'") ? text.slice(4) : text;
+ return json ? JSON.parse(json) : null;
+}
+
+export async function getChange(changeId: string): Promise<GerritChange | null> {
+ try {
+ return (await gerritFetch(`/changes/${encodeURIComponent(changeId)}`)) as GerritChange;
+ } catch {
+ return null;
+ }
+}
+
+export async function getChangeByNumber(changeNumber: number): Promise<GerritChange | null> {
+ try {
+ return (await gerritFetch(`/changes/${changeNumber}`)) as GerritChange;
+ } catch {
+ return null;
+ }
+}
+
+export async function postReview(changeId: string, review: GerritReview): Promise<void> {
+ await gerritFetch(`/changes/${encodeURIComponent(changeId)}/revisions/current/review`, {
+ method: 'POST',
+ body: JSON.stringify(review),
+ });
+}
+
+export async function getChangeDiff(changeId: string): Promise<string> {
+ const result = await gerritFetch(
+ `/changes/${encodeURIComponent(changeId)}/revisions/current/patch`
+ );
+ // Patch is base64 encoded
+ if (typeof result === 'string') {
+ return Buffer.from(result, 'base64').toString('utf-8');
+ }
+ return '';
+}
+
+export async function queryChanges(query: string): Promise<GerritChange[]> {
+ try {
+ return (await gerritFetch(`/changes/?q=${encodeURIComponent(query)}`)) as GerritChange[];
+ } catch {
+ return [];
+ }
+}
diff --git a/admin-panel/src/server/services/git.service.ts b/admin-panel/src/server/services/git.service.ts
new file mode 100644
index 0000000..990f5df
--- /dev/null
+++ b/admin-panel/src/server/services/git.service.ts
@@ -0,0 +1,142 @@
+import { execSync } from 'child_process';
+import { existsSync, mkdirSync, writeFileSync } from 'fs';
+import { join, dirname } from 'path';
+
+const REPO_BASE = process.env.GIT_REPO_PATH || '/repos';
+
+export interface RepoInfo {
+ name: string;
+ path: string;
+ exists: boolean;
+}
+
+export function createBareRepo(repoName: string, defaultBranch = 'main'): RepoInfo {
+ const repoPath = join(REPO_BASE, repoName);
+
+ if (existsSync(repoPath)) {
+ return { name: repoName, path: repoPath, exists: true };
+ }
+
+ execSync(`git init --bare "${repoPath}"`, { stdio: 'pipe' });
+ execSync(`git -C "${repoPath}" symbolic-ref HEAD refs/heads/${defaultBranch}`, {
+ stdio: 'pipe',
+ });
+
+ // Set description for cgit
+ const descPath = join(repoPath, 'description');
+ execSync(`echo "Repository managed by admin-panel" > "${descPath}"`, { stdio: 'pipe' });
+
+ // Enable post-update hook for git daemon
+ const hookPath = join(repoPath, 'hooks', 'post-update');
+ if (!existsSync(hookPath)) {
+ execSync(`cp "${repoPath}/hooks/post-update.sample" "${hookPath}" 2>/dev/null || true`, {
+ stdio: 'pipe',
+ });
+ }
+
+ // Run git update-server-info for HTTP access
+ execSync(`git -C "${repoPath}" update-server-info`, { stdio: 'pipe' });
+
+ return { name: repoName, path: repoPath, exists: false };
+}
+
+export function setRepoDescription(repoName: string, description: string): void {
+ const descPath = join(REPO_BASE, repoName, 'description');
+ execSync(`echo "${description.replace(/"/g, '\\"')}" > "${descPath}"`, { stdio: 'pipe' });
+}
+
+export function repoExists(repoName: string): boolean {
+ return existsSync(join(REPO_BASE, repoName));
+}
+
+export function deleteRepo(repoName: string): void {
+ const repoPath = join(REPO_BASE, repoName);
+ if (existsSync(repoPath)) {
+ execSync(`rm -rf "${repoPath}"`, { stdio: 'pipe' });
+ }
+}
+
+// --- Workspace operations for Builder agent ---
+
+export async function cloneRepo(bareRepoPath: string, workspacePath: string): Promise<void> {
+ if (existsSync(workspacePath)) {
+ // Pull latest instead of re-cloning
+ execSync(`git -C "${workspacePath}" fetch origin`, { stdio: 'pipe' });
+ execSync(`git -C "${workspacePath}" reset --hard origin/main 2>/dev/null || true`, { stdio: 'pipe' });
+ return;
+ }
+ mkdirSync(workspacePath, { recursive: true });
+ execSync(`git clone "${bareRepoPath}" "${workspacePath}"`, { stdio: 'pipe' });
+
+ // Configure git user for commits
+ execSync(`git -C "${workspacePath}" config user.email "admin-panel@swave.lol"`, { stdio: 'pipe' });
+ execSync(`git -C "${workspacePath}" config user.name "Admin Panel"`, { stdio: 'pipe' });
+}
+
+export async function createBranch(workspacePath: string, branchName: string): Promise<void> {
+ // Create or switch to branch
+ try {
+ execSync(`git -C "${workspacePath}" checkout -b "${branchName}"`, { stdio: 'pipe' });
+ } catch {
+ // Branch might already exist
+ execSync(`git -C "${workspacePath}" checkout "${branchName}"`, { stdio: 'pipe' });
+ }
+}
+
+export async function writeFiles(
+ workspacePath: string,
+ files: Array<{ path: string; content: string }>
+): Promise<void> {
+ for (const file of files) {
+ const fullPath = join(workspacePath, file.path);
+ mkdirSync(dirname(fullPath), { recursive: true });
+ writeFileSync(fullPath, file.content, 'utf-8');
+ }
+}
+
+export async function commitAndDiff(
+ workspacePath: string,
+ message: string,
+ branchName: string
+): Promise<string> {
+ execSync(`git -C "${workspacePath}" add -A`, { stdio: 'pipe' });
+
+ // Check if there are changes to commit
+ try {
+ execSync(`git -C "${workspacePath}" diff --cached --quiet`, { stdio: 'pipe' });
+ // No changes
+ return '';
+ } catch {
+ // There are changes — commit them
+ }
+
+ execSync(`git -C "${workspacePath}" commit -m "${message.replace(/"/g, '\\"')}"`, { stdio: 'pipe' });
+
+ // Get diff against main
+ const diff = execSync(
+ `git -C "${workspacePath}" diff main..${branchName} 2>/dev/null || git -C "${workspacePath}" diff HEAD~1..HEAD`,
+ { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 }
+ );
+
+ return diff;
+}
+
+export async function pushToGerrit(workspacePath: string): Promise<string> {
+ const output = execSync(
+ `git -C "${workspacePath}" push origin HEAD:refs/for/main 2>&1`,
+ { encoding: 'utf-8' }
+ );
+ return output;
+}
+
+export function getFileList(workspacePath: string): string {
+ if (!existsSync(workspacePath)) return '';
+ try {
+ return execSync(`find "${workspacePath}" -type f -not -path "*/.git/*" | sort`, {
+ encoding: 'utf-8',
+ maxBuffer: 1024 * 1024,
+ });
+ } catch {
+ return '';
+ }
+}
diff --git a/admin-panel/src/server/services/pipeline.service.ts b/admin-panel/src/server/services/pipeline.service.ts
new file mode 100644
index 0000000..afdf422
--- /dev/null
+++ b/admin-panel/src/server/services/pipeline.service.ts
@@ -0,0 +1,304 @@
+import { getProjectDb } from '../db.js';
+import { broadcast } from '../ws.js';
+import { BaseAgent, type AgentContext } from '../agents/base-agent.js';
+import { PlannerAgent } from '../agents/planner.js';
+import { CriticAgent } from '../agents/critic.js';
+import { BuilderAgent } from '../agents/builder.js';
+import { ShieldAgent } from '../agents/shield.js';
+import { InspectorAgent } from '../agents/inspector.js';
+import { RangerAgent } from '../agents/ranger.js';
+import { parseFileBlocks } from '../agents/builder.js';
+import { cloneRepo, createBranch, writeFiles, commitAndDiff } from './git.service.js';
+
+interface Task {
+ id: number;
+ title: string;
+ status: string;
+ priority: string;
+ level: number;
+ description: string | null;
+ plan: string | null;
+ decision_log: string | null;
+ done_when: string | null;
+ implementation_notes: string | null;
+ tags: string;
+ plan_review_comments: string;
+ review_comments: string;
+ test_results: string;
+ agent_log: string;
+ current_agent: string | null;
+ plan_review_count: number;
+ impl_review_count: number;
+ branch_name: string | null;
+ diff: string | null;
+ rank: number;
+}
+
+// Maps status → which agent runs
+const STATUS_AGENT_MAP: Record<string, () => BaseAgent> = {
+ todo: () => new PlannerAgent(),
+ plan: () => new PlannerAgent(),
+ plan_review: () => new CriticAgent(),
+ impl: () => new BuilderAgent(),
+ impl_review: () => new InspectorAgent(),
+ test: () => new RangerAgent(),
+};
+
+// Level determines which steps are skipped
+// L1 (quick): plan → impl → done (skip reviews and tests)
+// L2 (standard): plan → plan_review → impl → impl_review → done (skip tests)
+// L3 (full): all steps
+const LEVEL_FLOW: Record<number, string[]> = {
+ 1: ['todo', 'plan', 'impl', 'done'],
+ 2: ['todo', 'plan', 'plan_review', 'impl', 'impl_review', 'done'],
+ 3: ['todo', 'plan', 'plan_review', 'impl', 'impl_review', 'test', 'done'],
+};
+
+const MAX_RETRIES = 3;
+
+// Track running pipelines so we can stop them
+const runningPipelines = new Map<string, { stopped: boolean }>();
+
+function pipelineKey(project: string, taskId: number): string {
+ return `${project}:${taskId}`;
+}
+
+export function isPipelineRunning(project: string, taskId: number): boolean {
+ return runningPipelines.has(pipelineKey(project, taskId));
+}
+
+export function stopPipeline(project: string, taskId: number): void {
+ const key = pipelineKey(project, taskId);
+ const state = runningPipelines.get(key);
+ if (state) {
+ state.stopped = true;
+ }
+}
+
+export async function runStep(project: string, taskId: number): Promise<void> {
+ const db = getProjectDb(project);
+ const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId) as Task | undefined;
+ if (!task) throw new Error('Task not found');
+
+ if (task.status === 'done') throw new Error('Task is already done');
+
+ const agentFactory = STATUS_AGENT_MAP[task.status];
+ if (!agentFactory) throw new Error(`No agent for status: ${task.status}`);
+
+ const agent = agentFactory();
+ await executeAgent(project, task, agent, db);
+}
+
+export async function runFullPipeline(project: string, taskId: number): Promise<void> {
+ const key = pipelineKey(project, taskId);
+ if (runningPipelines.has(key)) throw new Error('Pipeline already running');
+
+ const state = { stopped: false };
+ runningPipelines.set(key, state);
+
+ try {
+ const db = getProjectDb(project);
+ let retries = 0;
+
+ while (!state.stopped) {
+ const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId) as Task | undefined;
+ if (!task) break;
+ if (task.status === 'done') break;
+
+ // Check if this status is in the level flow
+ const flow = LEVEL_FLOW[task.level] || LEVEL_FLOW[3];
+ if (!flow.includes(task.status)) {
+ // Skip to next valid status in the flow
+ const currentIdx = flow.indexOf(task.status);
+ if (currentIdx === -1) break;
+ }
+
+ const agentFactory = STATUS_AGENT_MAP[task.status];
+ if (!agentFactory) break;
+
+ const agent = agentFactory();
+
+ try {
+ const result = await executeAgent(project, task, agent, db);
+
+ // Check for review rejections (circuit breaker)
+ if (result.verdict === 'reject') {
+ retries++;
+ if (retries >= MAX_RETRIES) {
+ broadcast(project, {
+ type: 'pipeline:error',
+ project,
+ taskId,
+ error: `Circuit breaker: ${MAX_RETRIES} rejections reached. Pipeline stopped.`,
+ });
+ break;
+ }
+ } else {
+ retries = 0;
+ }
+ } catch (err) {
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ broadcast(project, {
+ type: 'pipeline:error',
+ project,
+ taskId,
+ error: errorMsg,
+ });
+ break;
+ }
+ }
+
+ broadcast(project, { type: 'pipeline:done', project, taskId });
+ } finally {
+ runningPipelines.delete(key);
+ }
+}
+
+async function executeAgent(
+ project: string,
+ task: Task,
+ agent: BaseAgent,
+ db: ReturnType<typeof getProjectDb>
+): Promise<{ verdict?: 'approve' | 'reject' }> {
+ // Create pipeline job
+ const job = db.prepare(
+ `INSERT INTO pipeline_jobs (task_id, agent, status, model, started_at)
+ VALUES (?, ?, 'running', ?, datetime('now'))`
+ ).run(task.id, agent.name, agent.model);
+ const jobId = job.lastInsertRowid;
+
+ // Update current agent on task
+ db.prepare('UPDATE tasks SET current_agent = ? WHERE id = ?').run(agent.name, task.id);
+ broadcast(project, { type: 'pipeline:agent_start', project, taskId: task.id, agent: agent.name });
+
+ try {
+ // Build context
+ const ctx: AgentContext = {
+ taskId: task.id,
+ title: task.title,
+ description: task.description || '',
+ plan: task.plan || undefined,
+ decisionLog: task.decision_log || undefined,
+ doneWhen: task.done_when || undefined,
+ implementationNotes: task.implementation_notes || undefined,
+ diff: task.diff || undefined,
+ testResults: task.test_results || undefined,
+ reviewComments: task.review_comments || undefined,
+ branchName: task.branch_name || undefined,
+ };
+
+ // Execute agent
+ const result = await agent.execute(ctx);
+
+ // Handle Builder agent's file changes
+ if (agent.name === 'builder' && result.content) {
+ const files = parseFileBlocks(result.content);
+ if (files.length > 0) {
+ try {
+ const branchName = task.branch_name || `kanban/task-${task.id}`;
+ const adminDb = (await import('../db.js')).getAdminDb();
+ const projectRow = adminDb.prepare('SELECT * FROM projects WHERE name = ?').get(project) as { repo_path: string } | undefined;
+
+ if (projectRow) {
+ const workspacePath = `/app/workspaces/${project}-task-${task.id}`;
+ await cloneRepo(projectRow.repo_path, workspacePath);
+ await createBranch(workspacePath, branchName);
+ await writeFiles(workspacePath, files);
+ const diff = await commitAndDiff(workspacePath, `feat: ${task.title} [kanban #${task.id}]`, branchName);
+ result.updates.branch_name = branchName;
+ result.updates.diff = diff;
+ }
+ } catch (gitErr) {
+ console.error('Git operation failed:', gitErr);
+ // Don't fail the whole pipeline over git errors
+ result.updates.implementation_notes =
+ (result.updates.implementation_notes || '') +
+ `\n\nGit error: ${gitErr instanceof Error ? gitErr.message : String(gitErr)}`;
+ }
+ }
+ }
+
+ // Apply updates to task
+ const updates = result.updates;
+ const setClauses: string[] = [];
+ const params: unknown[] = [];
+
+ for (const [key, value] of Object.entries(updates)) {
+ setClauses.push(`${key} = ?`);
+ params.push(value);
+ }
+
+ // Clear current_agent
+ setClauses.push('current_agent = NULL');
+
+ if (setClauses.length > 0) {
+ params.push(task.id);
+ db.prepare(`UPDATE tasks SET ${setClauses.join(', ')} WHERE id = ?`).run(...params);
+ }
+
+ // Append to agent log
+ const logEntry = {
+ agent: agent.name,
+ message: result.message,
+ verdict: result.verdict,
+ tokensUsed: result.tokensUsed,
+ model: result.model,
+ timestamp: new Date().toISOString(),
+ };
+
+ let agentLog: unknown[];
+ try {
+ agentLog = JSON.parse(task.agent_log || '[]');
+ } catch {
+ agentLog = [];
+ }
+ agentLog.push(logEntry);
+ db.prepare('UPDATE tasks SET agent_log = ? WHERE id = ?').run(JSON.stringify(agentLog), task.id);
+
+ // Update pipeline job
+ db.prepare(
+ `UPDATE pipeline_jobs SET status = 'completed', completed_at = datetime('now'), tokens_used = ? WHERE id = ?`
+ ).run(result.tokensUsed, jobId);
+
+ // Broadcast completion
+ const updatedTask = db.prepare('SELECT * FROM tasks WHERE id = ?').get(task.id);
+ broadcast(project, {
+ type: 'pipeline:agent_complete',
+ project,
+ taskId: task.id,
+ agent: agent.name,
+ result: result.message,
+ });
+ broadcast(project, { type: 'task:updated', project, task: updatedTask });
+
+ return { verdict: result.verdict };
+
+ } catch (err) {
+ // Mark job as failed
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ db.prepare(
+ `UPDATE pipeline_jobs SET status = 'failed', completed_at = datetime('now'), error = ? WHERE id = ?`
+ ).run(errorMsg, jobId);
+
+ // Clear current agent
+ db.prepare('UPDATE tasks SET current_agent = NULL WHERE id = ?').run(task.id);
+
+ throw err;
+ }
+}
+
+export function getPipelineStatus(project: string, taskId: number): {
+ running: boolean;
+ currentAgent: string | null;
+ jobs: unknown[];
+} {
+ const db = getProjectDb(project);
+ const task = db.prepare('SELECT current_agent FROM tasks WHERE id = ?').get(taskId) as { current_agent: string | null } | undefined;
+ const jobs = db.prepare('SELECT * FROM pipeline_jobs WHERE task_id = ? ORDER BY id DESC').all(taskId);
+
+ return {
+ running: isPipelineRunning(project, taskId),
+ currentAgent: task?.current_agent || null,
+ jobs,
+ };
+}
diff --git a/admin-panel/src/server/templates/builder.md b/admin-panel/src/server/templates/builder.md
new file mode 100644
index 0000000..eee2cf6
--- /dev/null
+++ b/admin-panel/src/server/templates/builder.md
@@ -0,0 +1,47 @@
+You are a senior software engineer implementing a feature based on an approved plan.
+
+## Task #{{taskId}}: {{title}}
+
+### Requirements
+{{description}}
+
+### Approved Plan
+{{plan}}
+
+### Done When
+{{done_when}}
+
+### Current Branch
+{{branch_name}}
+
+### Repository File Listing
+{{file_list}}
+
+### Relevant File Contents
+{{file_contents}}
+
+## Instructions
+
+Implement the changes described in the plan. For each file that needs to be created or modified, output the COMPLETE file contents.
+
+## Output Format
+
+For each file, use this exact format:
+
+```FILE: path/to/file.ts
+(complete file contents here)
+```
+
+After all files, provide a brief summary:
+
+### Summary
+(what you implemented and any notes)
+
+### Implementation Notes
+(any important details about the implementation)
+
+Important:
+- Output the COMPLETE contents of each file (not just the changes)
+- Include ALL imports, types, and code
+- Follow existing code style and conventions in the repo
+- Do not skip any file that needs changes
diff --git a/admin-panel/src/server/templates/critic.md b/admin-panel/src/server/templates/critic.md
new file mode 100644
index 0000000..dd68267
--- /dev/null
+++ b/admin-panel/src/server/templates/critic.md
@@ -0,0 +1,47 @@
+You are a critical plan reviewer. Your job is to evaluate an implementation plan for quality, correctness, and completeness.
+
+## Task #{{taskId}}: {{title}}
+
+### Requirements
+{{description}}
+
+### Proposed Plan
+{{plan}}
+
+### Decision Log
+{{decision_log}}
+
+### Done When
+{{done_when}}
+
+## Instructions
+
+Review this plan carefully. Consider:
+
+1. **Completeness**: Does the plan cover all requirements? Are any edge cases missed?
+2. **Correctness**: Is the technical approach sound? Any potential bugs or issues?
+3. **Simplicity**: Is the approach unnecessarily complex? Can it be simplified?
+4. **Security**: Any security concerns (injection, XSS, auth bypass)?
+5. **Testability**: Can the implementation be verified easily?
+
+## Output Format
+
+Start with your verdict on the FIRST line, exactly one of:
+```
+VERDICT: APPROVE
+```
+or
+```
+VERDICT: REJECT
+```
+
+Then provide your review:
+
+### Strengths
+- (what's good about this plan)
+
+### Issues
+- (any problems found — required for REJECT)
+
+### Suggestions
+- (improvements, even if approving)
diff --git a/admin-panel/src/server/templates/inspector.md b/admin-panel/src/server/templates/inspector.md
new file mode 100644
index 0000000..b392440
--- /dev/null
+++ b/admin-panel/src/server/templates/inspector.md
@@ -0,0 +1,61 @@
+You are a senior code reviewer. Your job is to review implemented code for quality, correctness, and adherence to the plan.
+
+## Task #{{taskId}}: {{title}}
+
+### Requirements
+{{description}}
+
+### Approved Plan
+{{plan}}
+
+### Done When
+{{done_when}}
+
+### Implementation Diff
+{{diff}}
+
+### Implementation Notes
+{{implementation_notes}}
+
+### Test Results
+{{test_results}}
+
+## Instructions
+
+Review the implementation diff carefully. Consider:
+
+1. **Correctness**: Does the code do what the plan describes? Any bugs?
+2. **Plan adherence**: Does the implementation match the plan?
+3. **Code quality**: Is the code clean, readable, and maintainable?
+4. **Security**: Any vulnerabilities (injection, XSS, auth issues)?
+5. **Performance**: Any obvious performance problems?
+6. **Error handling**: Are errors handled appropriately?
+7. **Test coverage**: Do the tests adequately verify the implementation?
+
+## Output Format
+
+Start with your verdict on the FIRST line, exactly one of:
+```
+VERDICT: APPROVE
+```
+or
+```
+VERDICT: REJECT
+```
+
+Then provide:
+
+### Score
+(1-10, where 10 is perfect)
+
+### Strengths
+- (what's done well)
+
+### Issues
+- (problems found — required for REJECT, include file:line references)
+
+### Suggestions
+- (improvements, even if approving)
+
+### Comments
+(line-specific comments for Gerrit, format: `file:line: comment`)
diff --git a/admin-panel/src/server/templates/planner.md b/admin-panel/src/server/templates/planner.md
new file mode 100644
index 0000000..5d1a2bb
--- /dev/null
+++ b/admin-panel/src/server/templates/planner.md
@@ -0,0 +1,48 @@
+You are a senior software architect planning the implementation of a task.
+
+## Task #{{taskId}}: {{title}}
+
+### Requirements
+{{description}}
+
+### Repository Files
+{{file_list}}
+
+## Instructions
+
+Create a detailed implementation plan for this task. Your plan should include:
+
+1. **Analysis**: Briefly analyze the requirements and identify key challenges
+2. **Approach**: Describe the technical approach you'll take
+3. **Steps**: Numbered list of concrete implementation steps
+4. **Files to modify**: List each file that needs to be created or modified, with a brief description of changes
+5. **Done When**: Clear acceptance criteria — when is this task complete?
+6. **Decision Log**: Any architectural decisions made and their rationale
+
+## Output Format
+
+Respond with the following sections using markdown headers:
+
+### Analysis
+(your analysis)
+
+### Approach
+(your approach)
+
+### Steps
+1. (step 1)
+2. (step 2)
+...
+
+### Files
+- `path/to/file.ts` — (what changes)
+...
+
+### Done When
+- (criterion 1)
+- (criterion 2)
+...
+
+### Decisions
+- (decision 1): (rationale)
+...
diff --git a/admin-panel/src/server/templates/ranger.md b/admin-panel/src/server/templates/ranger.md
new file mode 100644
index 0000000..74cc538
--- /dev/null
+++ b/admin-panel/src/server/templates/ranger.md
@@ -0,0 +1,50 @@
+You are a test runner and quality assurance agent. Your job is to evaluate test results and determine if the implementation passes.
+
+## Task #{{taskId}}: {{title}}
+
+### Done When
+{{done_when}}
+
+### Test Results
+{{test_results}}
+
+### Implementation Diff
+{{diff}}
+
+### Review Comments
+{{review_comments}}
+
+## Instructions
+
+Analyze the test results and review feedback. Determine if the task meets the "Done When" criteria.
+
+Consider:
+1. **Test pass/fail**: Did all tests pass?
+2. **Coverage**: Are the acceptance criteria covered by tests?
+3. **Review status**: Was the code review approved?
+4. **Quality**: Any remaining concerns?
+
+## Output Format
+
+Start with your verdict on the FIRST line, exactly one of:
+```
+VERDICT: PASS
+```
+or
+```
+VERDICT: FAIL
+```
+
+Then provide:
+
+### Summary
+(overall assessment)
+
+### Test Analysis
+- (breakdown of test results)
+
+### Remaining Issues
+- (any issues that prevent passing — required for FAIL)
+
+### Recommendation
+(what should happen next)
diff --git a/admin-panel/src/server/templates/shield.md b/admin-panel/src/server/templates/shield.md
new file mode 100644
index 0000000..27563ca
--- /dev/null
+++ b/admin-panel/src/server/templates/shield.md
@@ -0,0 +1,40 @@
+You are a TDD test engineer. Your job is to write comprehensive tests for an implementation.
+
+## Task #{{taskId}}: {{title}}
+
+### Requirements
+{{description}}
+
+### Done When
+{{done_when}}
+
+### Implementation Diff
+{{diff}}
+
+### Implementation Notes
+{{implementation_notes}}
+
+## Instructions
+
+Write tests that verify the implementation meets the requirements and "Done When" criteria. Consider:
+
+1. **Happy path**: Does the core functionality work?
+2. **Edge cases**: Boundary conditions, empty inputs, null values
+3. **Error cases**: Invalid inputs, missing data, failure modes
+4. **Integration**: Do components work together correctly?
+
+## Output Format
+
+For each test file, use this exact format:
+
+```FILE: path/to/test-file.test.ts
+(complete test file contents)
+```
+
+After the test files, provide:
+
+### Test Strategy
+- (what's being tested and why)
+
+### Coverage Notes
+- (what's covered, what's intentionally not covered)
diff --git a/admin-panel/src/server/ws.ts b/admin-panel/src/server/ws.ts
new file mode 100644
index 0000000..c0ffd8e
--- /dev/null
+++ b/admin-panel/src/server/ws.ts
@@ -0,0 +1,56 @@
+import { WebSocketServer, WebSocket } from 'ws';
+import type { Server } from 'http';
+
+let wss: WebSocketServer;
+
+interface Subscription {
+ ws: WebSocket;
+ projects: Set<string>;
+}
+
+const subscriptions: Subscription[] = [];
+
+export function setupWebSocket(server: Server): void {
+ wss = new WebSocketServer({ server, path: '/ws' });
+
+ wss.on('connection', (ws) => {
+ const sub: Subscription = { ws, projects: new Set() };
+ subscriptions.push(sub);
+
+ ws.on('message', (data) => {
+ try {
+ const msg = JSON.parse(data.toString());
+ if (msg.type === 'subscribe' && msg.project) {
+ sub.projects.add(msg.project);
+ } else if (msg.type === 'unsubscribe' && msg.project) {
+ sub.projects.delete(msg.project);
+ }
+ } catch {
+ // ignore malformed messages
+ }
+ });
+
+ ws.on('close', () => {
+ const idx = subscriptions.indexOf(sub);
+ if (idx !== -1) subscriptions.splice(idx, 1);
+ });
+ });
+}
+
+export function broadcast(project: string, event: Record<string, unknown>): void {
+ const payload = JSON.stringify(event);
+ for (const sub of subscriptions) {
+ if (sub.projects.has(project) && sub.ws.readyState === WebSocket.OPEN) {
+ sub.ws.send(payload);
+ }
+ }
+}
+
+export function broadcastAll(event: Record<string, unknown>): void {
+ const payload = JSON.stringify(event);
+ for (const sub of subscriptions) {
+ if (sub.ws.readyState === WebSocket.OPEN) {
+ sub.ws.send(payload);
+ }
+ }
+}
diff --git a/admin-panel/tsconfig.json b/admin-panel/tsconfig.json
new file mode 100644
index 0000000..b73e854
--- /dev/null
+++ b/admin-panel/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/admin-panel/tsconfig.server.json b/admin-panel/tsconfig.server.json
new file mode 100644
index 0000000..d616782
--- /dev/null
+++ b/admin-panel/tsconfig.server.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "declaration": false,
+ "sourceMap": true
+ },
+ "include": ["src/server/**/*"],
+ "exclude": ["node_modules", "dist", "src/client"]
+}
diff --git a/admin-panel/vite.config.ts b/admin-panel/vite.config.ts
new file mode 100644
index 0000000..0cbe3b2
--- /dev/null
+++ b/admin-panel/vite.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig } from 'vite';
+import { resolve } from 'path';
+
+export default defineConfig({
+ root: 'src/client',
+ build: {
+ outDir: '../../dist/client',
+ emptyOutDir: true,
+ },
+ server: {
+ proxy: {
+ '/api': 'http://localhost:3000',
+ '/ws': {
+ target: 'ws://localhost:3000',
+ ws: true,
+ },
+ },
+ },
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, 'src/client'),
+ },
+ },
+});
diff --git a/nginx/docker-compose.yaml b/nginx/docker-compose.yaml
index 86dfc74..922df75 100644
--- a/nginx/docker-compose.yaml
+++ b/nginx/docker-compose.yaml
@@ -24,6 +24,7 @@ services:
monitoring_network:
nexus_network:
authelia_network:
+ admin_network:
networks:
#write each stack network (or connect it manually later)
@@ -45,4 +46,7 @@ networks:
authelia_network:
name: authelia_network
external: true
+ admin_network:
+ name: admin_network
+ external: true
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index 2c9f4cf..52bb590 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -45,7 +45,7 @@ server {
listen 80;
listen [::]:80;
- server_name swave.lol blog.swave.lol ghost.swave.lol jenkins.swave.lol cgit.swave.lol gerrit.swave.lol nexus.swave.lol registry.swave.lol auth.swave.lol;
+ server_name swave.lol blog.swave.lol ghost.swave.lol jenkins.swave.lol cgit.swave.lol gerrit.swave.lol nexus.swave.lol registry.swave.lol auth.swave.lol admin.swave.lol;
location / {
rewrite ^ https://$host$request_uri? permanent;
@@ -524,3 +524,82 @@ server {
proxy_set_header X-Forwarded-Host $http_host;
}
}
+
+# Admin Panel — admin.swave.lol
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+
+ server_name admin.swave.lol;
+
+ server_tokens off;
+
+ ssl_certificate /var/letsencrypt/etc/live/swave.lol/fullchain.pem;
+ ssl_certificate_key /var/letsencrypt/etc/live/swave.lol/privkey.pem;
+
+ ssl_buffer_size 8k;
+
+ ssl_dhparam /etc/ssl/certs/dhparam-2048.pem;
+
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_prefer_server_ciphers on;
+
+ ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
+
+ ssl_ecdh_curve secp384r1;
+ ssl_session_tickets off;
+
+ # OCSP stapling
+ ssl_stapling on;
+ ssl_stapling_verify on;
+ resolver 8.8.8.8;
+
+ charset utf-8;
+
+ # WebSocket support
+ location /ws {
+ auth_request /_authelia-auth;
+ auth_request_set $authelia_user $upstream_http_remote_user;
+ proxy_pass http://admin-panel:3000/ws;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_read_timeout 86400;
+ }
+
+ location / {
+ auth_request /_authelia-auth;
+ auth_request_set $authelia_user $upstream_http_remote_user;
+ proxy_pass http://admin-panel:3000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # Authelia forward-auth subrequest endpoint
+ location = /_authelia-auth {
+ internal;
+ resolver 127.0.0.11 valid=30s;
+ set $authelia_upstream http://authelia:9091/api/verify;
+ proxy_pass $authelia_upstream;
+ proxy_pass_request_body off;
+ proxy_set_header Content-Length "";
+ proxy_set_header Cookie $http_cookie;
+ proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
+ proxy_set_header X-Forwarded-Method $request_method;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $http_host;
+ proxy_set_header X-Forwarded-Uri $request_uri;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ error_page 401 = @authelia_login_redirect;
+ location @authelia_login_redirect {
+ return 302 https://auth.swave.lol/?rd=$scheme://$http_host$request_uri;
+ }
+}