Spaces:
Runtime error
Runtime error
Nyk commited on
Commit ·
b608851
1
Parent(s): 7606386
feat: add Docker support, session controls, model catalog, API rate limiting
Browse files- .dockerignore +9 -0
- Dockerfile +23 -0
- docker-compose.yml +12 -0
- next.config.js +4 -0
- package.json +3 -0
- pnpm-lock.yaml +161 -0
- src/app/api/activities/route.ts +1 -1
- src/app/api/agents/route.ts +21 -10
- src/app/api/alerts/route.ts +18 -14
- src/app/api/audit/route.ts +1 -1
- src/app/api/auth/login/route.ts +5 -19
- src/app/api/export/route.ts +12 -4
- src/app/api/notifications/route.ts +11 -1
- src/app/api/sessions/[id]/control/route.ts +61 -0
- src/app/api/settings/route.ts +7 -0
- src/app/api/spawn/route.ts +9 -4
- src/app/api/status/route.ts +3 -11
- src/app/api/tasks/[id]/route.ts +15 -4
- src/app/api/tasks/route.ts +15 -8
- src/app/api/webhooks/route.ts +21 -18
- src/app/login/page.tsx +3 -1
- src/app/page.tsx +7 -2
- src/components/ErrorBoundary.tsx +58 -0
- src/components/layout/header-bar.tsx +1 -1
- src/components/layout/nav-rail.tsx +4 -0
- src/components/panels/session-details-panel.tsx +61 -16
- src/lib/db.ts +5 -4
- src/lib/logger.ts +11 -0
- src/lib/models.ts +30 -0
- src/lib/rate-limit.ts +64 -0
- src/lib/scheduler.ts +3 -2
- src/lib/validation.ts +74 -0
- src/lib/webhooks.ts +4 -3
- src/store/index.ts +2 -10
.dockerignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
.git
|
| 3 |
+
.data
|
| 4 |
+
.next
|
| 5 |
+
.env
|
| 6 |
+
*.md
|
| 7 |
+
.github
|
| 8 |
+
ops
|
| 9 |
+
scripts
|
Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-slim AS base
|
| 2 |
+
RUN corepack enable && corepack prepare pnpm@latest --activate
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
FROM base AS deps
|
| 6 |
+
COPY package.json pnpm-lock.yaml ./
|
| 7 |
+
RUN pnpm install --frozen-lockfile
|
| 8 |
+
|
| 9 |
+
FROM base AS build
|
| 10 |
+
COPY --from=deps /app/node_modules ./node_modules
|
| 11 |
+
COPY . .
|
| 12 |
+
RUN pnpm build
|
| 13 |
+
|
| 14 |
+
FROM node:20-slim AS runtime
|
| 15 |
+
WORKDIR /app
|
| 16 |
+
ENV NODE_ENV=production
|
| 17 |
+
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
| 18 |
+
COPY --from=build /app/.next/standalone ./
|
| 19 |
+
COPY --from=build /app/.next/static ./.next/static
|
| 20 |
+
COPY --from=build /app/public ./public
|
| 21 |
+
USER nextjs
|
| 22 |
+
EXPOSE 3000
|
| 23 |
+
CMD ["node", "server.js"]
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
mission-control:
|
| 3 |
+
build: .
|
| 4 |
+
ports:
|
| 5 |
+
- "3000:3000"
|
| 6 |
+
env_file: .env
|
| 7 |
+
volumes:
|
| 8 |
+
- mc-data:/app/.data
|
| 9 |
+
restart: unless-stopped
|
| 10 |
+
|
| 11 |
+
volumes:
|
| 12 |
+
mc-data:
|
next.config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
/** @type {import('next').NextConfig} */
|
| 2 |
const nextConfig = {
|
|
|
|
| 3 |
turbopack: {},
|
| 4 |
|
| 5 |
// Security headers
|
|
@@ -25,6 +26,9 @@ const nextConfig = {
|
|
| 25 |
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
| 26 |
{ key: 'Content-Security-Policy', value: csp },
|
| 27 |
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
|
|
|
|
|
|
|
|
|
|
| 28 |
],
|
| 29 |
},
|
| 30 |
];
|
|
|
|
| 1 |
/** @type {import('next').NextConfig} */
|
| 2 |
const nextConfig = {
|
| 3 |
+
output: 'standalone',
|
| 4 |
turbopack: {},
|
| 5 |
|
| 6 |
// Security headers
|
|
|
|
| 26 |
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
| 27 |
{ key: 'Content-Security-Policy', value: csp },
|
| 28 |
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
|
| 29 |
+
...(process.env.MC_ENABLE_HSTS === '1' ? [
|
| 30 |
+
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }
|
| 31 |
+
] : []),
|
| 32 |
],
|
| 33 |
},
|
| 34 |
];
|
package.json
CHANGED
|
@@ -24,6 +24,7 @@
|
|
| 24 |
"eslint-config-next": "^16.1.6",
|
| 25 |
"next": "^16.1.6",
|
| 26 |
"next-themes": "^0.4.6",
|
|
|
|
| 27 |
"postcss": "^8.5.2",
|
| 28 |
"react": "^19.0.1",
|
| 29 |
"react-dom": "^19.0.1",
|
|
@@ -33,6 +34,7 @@
|
|
| 33 |
"tailwindcss": "^3.4.17",
|
| 34 |
"typescript": "^5.7.2",
|
| 35 |
"ws": "^8.19.0",
|
|
|
|
| 36 |
"zustand": "^5.0.11"
|
| 37 |
},
|
| 38 |
"devDependencies": {
|
|
@@ -47,6 +49,7 @@
|
|
| 47 |
"@types/ws": "^8.18.1",
|
| 48 |
"@vitejs/plugin-react": "^4.3.4",
|
| 49 |
"jsdom": "^26.0.0",
|
|
|
|
| 50 |
"vite-tsconfig-paths": "^5.1.4",
|
| 51 |
"vitest": "^2.1.5"
|
| 52 |
},
|
|
|
|
| 24 |
"eslint-config-next": "^16.1.6",
|
| 25 |
"next": "^16.1.6",
|
| 26 |
"next-themes": "^0.4.6",
|
| 27 |
+
"pino": "^10.3.1",
|
| 28 |
"postcss": "^8.5.2",
|
| 29 |
"react": "^19.0.1",
|
| 30 |
"react-dom": "^19.0.1",
|
|
|
|
| 34 |
"tailwindcss": "^3.4.17",
|
| 35 |
"typescript": "^5.7.2",
|
| 36 |
"ws": "^8.19.0",
|
| 37 |
+
"zod": "^4.3.6",
|
| 38 |
"zustand": "^5.0.11"
|
| 39 |
},
|
| 40 |
"devDependencies": {
|
|
|
|
| 49 |
"@types/ws": "^8.18.1",
|
| 50 |
"@vitejs/plugin-react": "^4.3.4",
|
| 51 |
"jsdom": "^26.0.0",
|
| 52 |
+
"pino-pretty": "^13.1.3",
|
| 53 |
"vite-tsconfig-paths": "^5.1.4",
|
| 54 |
"vitest": "^2.1.5"
|
| 55 |
},
|
pnpm-lock.yaml
CHANGED
|
@@ -32,6 +32,9 @@ importers:
|
|
| 32 |
next-themes:
|
| 33 |
specifier: ^0.4.6
|
| 34 |
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
|
|
|
|
|
|
|
|
|
| 35 |
postcss:
|
| 36 |
specifier: ^8.5.2
|
| 37 |
version: 8.5.6
|
|
@@ -59,6 +62,9 @@ importers:
|
|
| 59 |
ws:
|
| 60 |
specifier: ^8.19.0
|
| 61 |
version: 8.19.0
|
|
|
|
|
|
|
|
|
|
| 62 |
zustand:
|
| 63 |
specifier: ^5.0.11
|
| 64 |
version: 5.0.11(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
|
|
@@ -96,6 +102,9 @@ importers:
|
|
| 96 |
jsdom:
|
| 97 |
specifier: ^26.0.0
|
| 98 |
version: 26.1.0
|
|
|
|
|
|
|
|
|
|
| 99 |
vite-tsconfig-paths:
|
| 100 |
specifier: ^5.1.4
|
| 101 |
version: 5.1.4(typescript@5.9.3)(vite@5.4.21(@types/node@22.19.9))
|
|
@@ -677,6 +686,9 @@ packages:
|
|
| 677 |
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
| 678 |
engines: {node: '>=12.4.0'}
|
| 679 |
|
|
|
|
|
|
|
|
|
|
| 680 |
'@playwright/test@1.58.2':
|
| 681 |
resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
|
| 682 |
engines: {node: '>=18'}
|
|
@@ -1346,6 +1358,10 @@ packages:
|
|
| 1346 |
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
| 1347 |
engines: {node: '>= 0.4'}
|
| 1348 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1349 |
autoprefixer@10.4.24:
|
| 1350 |
resolution: {integrity: sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==}
|
| 1351 |
engines: {node: ^10 || ^12 || >=14}
|
|
@@ -1470,6 +1486,9 @@ packages:
|
|
| 1470 |
color-name@1.1.4:
|
| 1471 |
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
| 1472 |
|
|
|
|
|
|
|
|
|
|
| 1473 |
commander@4.1.1:
|
| 1474 |
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
| 1475 |
engines: {node: '>= 6'}
|
|
@@ -1584,6 +1603,9 @@ packages:
|
|
| 1584 |
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
| 1585 |
engines: {node: '>= 0.4'}
|
| 1586 |
|
|
|
|
|
|
|
|
|
|
| 1587 |
debug@3.2.7:
|
| 1588 |
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
| 1589 |
peerDependencies:
|
|
@@ -1852,6 +1874,9 @@ packages:
|
|
| 1852 |
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
| 1853 |
engines: {node: '>=12.0.0'}
|
| 1854 |
|
|
|
|
|
|
|
|
|
|
| 1855 |
fast-deep-equal@3.1.3:
|
| 1856 |
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
| 1857 |
|
|
@@ -1869,6 +1894,9 @@ packages:
|
|
| 1869 |
fast-levenshtein@2.0.6:
|
| 1870 |
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
| 1871 |
|
|
|
|
|
|
|
|
|
|
| 1872 |
fastq@1.20.1:
|
| 1873 |
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
|
| 1874 |
|
|
@@ -2013,6 +2041,9 @@ packages:
|
|
| 2013 |
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
| 2014 |
engines: {node: '>= 0.4'}
|
| 2015 |
|
|
|
|
|
|
|
|
|
|
| 2016 |
hermes-estree@0.25.1:
|
| 2017 |
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
|
| 2018 |
|
|
@@ -2202,6 +2233,10 @@ packages:
|
|
| 2202 |
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
| 2203 |
hasBin: true
|
| 2204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2205 |
js-tokens@4.0.0:
|
| 2206 |
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
| 2207 |
|
|
@@ -2425,6 +2460,10 @@ packages:
|
|
| 2425 |
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
| 2426 |
engines: {node: '>= 0.4'}
|
| 2427 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2428 |
once@1.4.0:
|
| 2429 |
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
| 2430 |
|
|
@@ -2484,6 +2523,20 @@ packages:
|
|
| 2484 |
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
| 2485 |
engines: {node: '>=0.10.0'}
|
| 2486 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2487 |
pirates@4.0.7:
|
| 2488 |
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
| 2489 |
engines: {node: '>= 6'}
|
|
@@ -2566,6 +2619,9 @@ packages:
|
|
| 2566 |
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
| 2567 |
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
| 2568 |
|
|
|
|
|
|
|
|
|
|
| 2569 |
prop-types@15.8.1:
|
| 2570 |
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
| 2571 |
|
|
@@ -2579,6 +2635,9 @@ packages:
|
|
| 2579 |
queue-microtask@1.2.3:
|
| 2580 |
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
| 2581 |
|
|
|
|
|
|
|
|
|
|
| 2582 |
rc@1.2.8:
|
| 2583 |
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
| 2584 |
hasBin: true
|
|
@@ -2631,6 +2690,10 @@ packages:
|
|
| 2631 |
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
| 2632 |
engines: {node: '>=8.10.0'}
|
| 2633 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2634 |
recharts@3.7.0:
|
| 2635 |
resolution: {integrity: sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==}
|
| 2636 |
engines: {node: '>=18'}
|
|
@@ -2708,6 +2771,10 @@ packages:
|
|
| 2708 |
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
| 2709 |
engines: {node: '>= 0.4'}
|
| 2710 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2711 |
safer-buffer@2.1.2:
|
| 2712 |
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
| 2713 |
|
|
@@ -2718,6 +2785,9 @@ packages:
|
|
| 2718 |
scheduler@0.27.0:
|
| 2719 |
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
| 2720 |
|
|
|
|
|
|
|
|
|
|
| 2721 |
semver@6.3.1:
|
| 2722 |
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
| 2723 |
hasBin: true
|
|
@@ -2776,10 +2846,17 @@ packages:
|
|
| 2776 |
simple-get@4.0.1:
|
| 2777 |
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
| 2778 |
|
|
|
|
|
|
|
|
|
|
| 2779 |
source-map-js@1.2.1:
|
| 2780 |
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
| 2781 |
engines: {node: '>=0.10.0'}
|
| 2782 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2783 |
stable-hash@0.0.5:
|
| 2784 |
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
|
| 2785 |
|
|
@@ -2835,6 +2912,10 @@ packages:
|
|
| 2835 |
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
| 2836 |
engines: {node: '>=8'}
|
| 2837 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2838 |
styled-jsx@5.1.6:
|
| 2839 |
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
|
| 2840 |
engines: {node: '>= 12.0.0'}
|
|
@@ -2886,6 +2967,10 @@ packages:
|
|
| 2886 |
thenify@3.3.1:
|
| 2887 |
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
| 2888 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2889 |
tiny-invariant@1.3.3:
|
| 2890 |
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
| 2891 |
|
|
@@ -3668,6 +3753,8 @@ snapshots:
|
|
| 3668 |
|
| 3669 |
'@nolyfill/is-core-module@1.0.39': {}
|
| 3670 |
|
|
|
|
|
|
|
| 3671 |
'@playwright/test@1.58.2':
|
| 3672 |
dependencies:
|
| 3673 |
playwright: 1.58.2
|
|
@@ -4392,6 +4479,8 @@ snapshots:
|
|
| 4392 |
|
| 4393 |
async-function@1.0.0: {}
|
| 4394 |
|
|
|
|
|
|
|
| 4395 |
autoprefixer@10.4.24(postcss@8.5.6):
|
| 4396 |
dependencies:
|
| 4397 |
browserslist: 4.28.1
|
|
@@ -4524,6 +4613,8 @@ snapshots:
|
|
| 4524 |
|
| 4525 |
color-name@1.1.4: {}
|
| 4526 |
|
|
|
|
|
|
|
| 4527 |
commander@4.1.1: {}
|
| 4528 |
|
| 4529 |
concat-map@0.0.1: {}
|
|
@@ -4636,6 +4727,8 @@ snapshots:
|
|
| 4636 |
es-errors: 1.3.0
|
| 4637 |
is-data-view: 1.0.2
|
| 4638 |
|
|
|
|
|
|
|
| 4639 |
debug@3.2.7:
|
| 4640 |
dependencies:
|
| 4641 |
ms: 2.1.3
|
|
@@ -5050,6 +5143,8 @@ snapshots:
|
|
| 5050 |
|
| 5051 |
expect-type@1.3.0: {}
|
| 5052 |
|
|
|
|
|
|
|
| 5053 |
fast-deep-equal@3.1.3: {}
|
| 5054 |
|
| 5055 |
fast-glob@3.3.1:
|
|
@@ -5072,6 +5167,8 @@ snapshots:
|
|
| 5072 |
|
| 5073 |
fast-levenshtein@2.0.6: {}
|
| 5074 |
|
|
|
|
|
|
|
| 5075 |
fastq@1.20.1:
|
| 5076 |
dependencies:
|
| 5077 |
reusify: 1.1.0
|
|
@@ -5206,6 +5303,8 @@ snapshots:
|
|
| 5206 |
dependencies:
|
| 5207 |
function-bind: 1.1.2
|
| 5208 |
|
|
|
|
|
|
|
| 5209 |
hermes-estree@0.25.1: {}
|
| 5210 |
|
| 5211 |
hermes-parser@0.25.1:
|
|
@@ -5398,6 +5497,8 @@ snapshots:
|
|
| 5398 |
|
| 5399 |
jiti@1.21.7: {}
|
| 5400 |
|
|
|
|
|
|
|
| 5401 |
js-tokens@4.0.0: {}
|
| 5402 |
|
| 5403 |
js-yaml@4.1.1:
|
|
@@ -5620,6 +5721,8 @@ snapshots:
|
|
| 5620 |
define-properties: 1.2.1
|
| 5621 |
es-object-atoms: 1.1.1
|
| 5622 |
|
|
|
|
|
|
|
| 5623 |
once@1.4.0:
|
| 5624 |
dependencies:
|
| 5625 |
wrappy: 1.0.2
|
|
@@ -5673,6 +5776,42 @@ snapshots:
|
|
| 5673 |
|
| 5674 |
pify@2.3.0: {}
|
| 5675 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5676 |
pirates@4.0.7: {}
|
| 5677 |
|
| 5678 |
playwright-core@1.58.2: {}
|
|
@@ -5751,6 +5890,8 @@ snapshots:
|
|
| 5751 |
ansi-styles: 5.2.0
|
| 5752 |
react-is: 17.0.2
|
| 5753 |
|
|
|
|
|
|
|
| 5754 |
prop-types@15.8.1:
|
| 5755 |
dependencies:
|
| 5756 |
loose-envify: 1.4.0
|
|
@@ -5766,6 +5907,8 @@ snapshots:
|
|
| 5766 |
|
| 5767 |
queue-microtask@1.2.3: {}
|
| 5768 |
|
|
|
|
|
|
|
| 5769 |
rc@1.2.8:
|
| 5770 |
dependencies:
|
| 5771 |
deep-extend: 0.6.0
|
|
@@ -5823,6 +5966,8 @@ snapshots:
|
|
| 5823 |
dependencies:
|
| 5824 |
picomatch: 2.3.1
|
| 5825 |
|
|
|
|
|
|
|
| 5826 |
recharts@3.7.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react-is@17.0.2)(react@19.2.4)(redux@5.0.1):
|
| 5827 |
dependencies:
|
| 5828 |
'@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.13)(react@19.2.4)(redux@5.0.1))(react@19.2.4)
|
|
@@ -5952,6 +6097,8 @@ snapshots:
|
|
| 5952 |
es-errors: 1.3.0
|
| 5953 |
is-regex: 1.2.1
|
| 5954 |
|
|
|
|
|
|
|
| 5955 |
safer-buffer@2.1.2: {}
|
| 5956 |
|
| 5957 |
saxes@6.0.0:
|
|
@@ -5960,6 +6107,8 @@ snapshots:
|
|
| 5960 |
|
| 5961 |
scheduler@0.27.0: {}
|
| 5962 |
|
|
|
|
|
|
|
| 5963 |
semver@6.3.1: {}
|
| 5964 |
|
| 5965 |
semver@7.7.4: {}
|
|
@@ -6062,8 +6211,14 @@ snapshots:
|
|
| 6062 |
once: 1.4.0
|
| 6063 |
simple-concat: 1.0.1
|
| 6064 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6065 |
source-map-js@1.2.1: {}
|
| 6066 |
|
|
|
|
|
|
|
| 6067 |
stable-hash@0.0.5: {}
|
| 6068 |
|
| 6069 |
stackback@0.0.2: {}
|
|
@@ -6139,6 +6294,8 @@ snapshots:
|
|
| 6139 |
|
| 6140 |
strip-json-comments@3.1.1: {}
|
| 6141 |
|
|
|
|
|
|
|
| 6142 |
styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4):
|
| 6143 |
dependencies:
|
| 6144 |
client-only: 0.0.1
|
|
@@ -6217,6 +6374,10 @@ snapshots:
|
|
| 6217 |
dependencies:
|
| 6218 |
any-promise: 1.3.0
|
| 6219 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6220 |
tiny-invariant@1.3.3: {}
|
| 6221 |
|
| 6222 |
tinybench@2.9.0: {}
|
|
|
|
| 32 |
next-themes:
|
| 33 |
specifier: ^0.4.6
|
| 34 |
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
| 35 |
+
pino:
|
| 36 |
+
specifier: ^10.3.1
|
| 37 |
+
version: 10.3.1
|
| 38 |
postcss:
|
| 39 |
specifier: ^8.5.2
|
| 40 |
version: 8.5.6
|
|
|
|
| 62 |
ws:
|
| 63 |
specifier: ^8.19.0
|
| 64 |
version: 8.19.0
|
| 65 |
+
zod:
|
| 66 |
+
specifier: ^4.3.6
|
| 67 |
+
version: 4.3.6
|
| 68 |
zustand:
|
| 69 |
specifier: ^5.0.11
|
| 70 |
version: 5.0.11(@types/react@19.2.13)(immer@11.1.3)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
|
|
|
|
| 102 |
jsdom:
|
| 103 |
specifier: ^26.0.0
|
| 104 |
version: 26.1.0
|
| 105 |
+
pino-pretty:
|
| 106 |
+
specifier: ^13.1.3
|
| 107 |
+
version: 13.1.3
|
| 108 |
vite-tsconfig-paths:
|
| 109 |
specifier: ^5.1.4
|
| 110 |
version: 5.1.4(typescript@5.9.3)(vite@5.4.21(@types/node@22.19.9))
|
|
|
|
| 686 |
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
| 687 |
engines: {node: '>=12.4.0'}
|
| 688 |
|
| 689 |
+
'@pinojs/redact@0.4.0':
|
| 690 |
+
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
| 691 |
+
|
| 692 |
'@playwright/test@1.58.2':
|
| 693 |
resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
|
| 694 |
engines: {node: '>=18'}
|
|
|
|
| 1358 |
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
| 1359 |
engines: {node: '>= 0.4'}
|
| 1360 |
|
| 1361 |
+
atomic-sleep@1.0.0:
|
| 1362 |
+
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
| 1363 |
+
engines: {node: '>=8.0.0'}
|
| 1364 |
+
|
| 1365 |
autoprefixer@10.4.24:
|
| 1366 |
resolution: {integrity: sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==}
|
| 1367 |
engines: {node: ^10 || ^12 || >=14}
|
|
|
|
| 1486 |
color-name@1.1.4:
|
| 1487 |
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
| 1488 |
|
| 1489 |
+
colorette@2.0.20:
|
| 1490 |
+
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
| 1491 |
+
|
| 1492 |
commander@4.1.1:
|
| 1493 |
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
| 1494 |
engines: {node: '>= 6'}
|
|
|
|
| 1603 |
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
| 1604 |
engines: {node: '>= 0.4'}
|
| 1605 |
|
| 1606 |
+
dateformat@4.6.3:
|
| 1607 |
+
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
| 1608 |
+
|
| 1609 |
debug@3.2.7:
|
| 1610 |
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
| 1611 |
peerDependencies:
|
|
|
|
| 1874 |
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
| 1875 |
engines: {node: '>=12.0.0'}
|
| 1876 |
|
| 1877 |
+
fast-copy@4.0.2:
|
| 1878 |
+
resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==}
|
| 1879 |
+
|
| 1880 |
fast-deep-equal@3.1.3:
|
| 1881 |
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
| 1882 |
|
|
|
|
| 1894 |
fast-levenshtein@2.0.6:
|
| 1895 |
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
| 1896 |
|
| 1897 |
+
fast-safe-stringify@2.1.1:
|
| 1898 |
+
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
|
| 1899 |
+
|
| 1900 |
fastq@1.20.1:
|
| 1901 |
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
|
| 1902 |
|
|
|
|
| 2041 |
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
| 2042 |
engines: {node: '>= 0.4'}
|
| 2043 |
|
| 2044 |
+
help-me@5.0.0:
|
| 2045 |
+
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
| 2046 |
+
|
| 2047 |
hermes-estree@0.25.1:
|
| 2048 |
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
|
| 2049 |
|
|
|
|
| 2233 |
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
| 2234 |
hasBin: true
|
| 2235 |
|
| 2236 |
+
joycon@3.1.1:
|
| 2237 |
+
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
| 2238 |
+
engines: {node: '>=10'}
|
| 2239 |
+
|
| 2240 |
js-tokens@4.0.0:
|
| 2241 |
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
| 2242 |
|
|
|
|
| 2460 |
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
| 2461 |
engines: {node: '>= 0.4'}
|
| 2462 |
|
| 2463 |
+
on-exit-leak-free@2.1.2:
|
| 2464 |
+
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
| 2465 |
+
engines: {node: '>=14.0.0'}
|
| 2466 |
+
|
| 2467 |
once@1.4.0:
|
| 2468 |
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
| 2469 |
|
|
|
|
| 2523 |
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
| 2524 |
engines: {node: '>=0.10.0'}
|
| 2525 |
|
| 2526 |
+
pino-abstract-transport@3.0.0:
|
| 2527 |
+
resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==}
|
| 2528 |
+
|
| 2529 |
+
pino-pretty@13.1.3:
|
| 2530 |
+
resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==}
|
| 2531 |
+
hasBin: true
|
| 2532 |
+
|
| 2533 |
+
pino-std-serializers@7.1.0:
|
| 2534 |
+
resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==}
|
| 2535 |
+
|
| 2536 |
+
pino@10.3.1:
|
| 2537 |
+
resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==}
|
| 2538 |
+
hasBin: true
|
| 2539 |
+
|
| 2540 |
pirates@4.0.7:
|
| 2541 |
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
| 2542 |
engines: {node: '>= 6'}
|
|
|
|
| 2619 |
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
| 2620 |
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
| 2621 |
|
| 2622 |
+
process-warning@5.0.0:
|
| 2623 |
+
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
| 2624 |
+
|
| 2625 |
prop-types@15.8.1:
|
| 2626 |
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
| 2627 |
|
|
|
|
| 2635 |
queue-microtask@1.2.3:
|
| 2636 |
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
| 2637 |
|
| 2638 |
+
quick-format-unescaped@4.0.4:
|
| 2639 |
+
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
| 2640 |
+
|
| 2641 |
rc@1.2.8:
|
| 2642 |
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
| 2643 |
hasBin: true
|
|
|
|
| 2690 |
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
| 2691 |
engines: {node: '>=8.10.0'}
|
| 2692 |
|
| 2693 |
+
real-require@0.2.0:
|
| 2694 |
+
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
| 2695 |
+
engines: {node: '>= 12.13.0'}
|
| 2696 |
+
|
| 2697 |
recharts@3.7.0:
|
| 2698 |
resolution: {integrity: sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==}
|
| 2699 |
engines: {node: '>=18'}
|
|
|
|
| 2771 |
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
| 2772 |
engines: {node: '>= 0.4'}
|
| 2773 |
|
| 2774 |
+
safe-stable-stringify@2.5.0:
|
| 2775 |
+
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
| 2776 |
+
engines: {node: '>=10'}
|
| 2777 |
+
|
| 2778 |
safer-buffer@2.1.2:
|
| 2779 |
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
| 2780 |
|
|
|
|
| 2785 |
scheduler@0.27.0:
|
| 2786 |
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
| 2787 |
|
| 2788 |
+
secure-json-parse@4.1.0:
|
| 2789 |
+
resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==}
|
| 2790 |
+
|
| 2791 |
semver@6.3.1:
|
| 2792 |
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
| 2793 |
hasBin: true
|
|
|
|
| 2846 |
simple-get@4.0.1:
|
| 2847 |
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
| 2848 |
|
| 2849 |
+
sonic-boom@4.2.1:
|
| 2850 |
+
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
|
| 2851 |
+
|
| 2852 |
source-map-js@1.2.1:
|
| 2853 |
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
| 2854 |
engines: {node: '>=0.10.0'}
|
| 2855 |
|
| 2856 |
+
split2@4.2.0:
|
| 2857 |
+
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
| 2858 |
+
engines: {node: '>= 10.x'}
|
| 2859 |
+
|
| 2860 |
stable-hash@0.0.5:
|
| 2861 |
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
|
| 2862 |
|
|
|
|
| 2912 |
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
| 2913 |
engines: {node: '>=8'}
|
| 2914 |
|
| 2915 |
+
strip-json-comments@5.0.3:
|
| 2916 |
+
resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
|
| 2917 |
+
engines: {node: '>=14.16'}
|
| 2918 |
+
|
| 2919 |
styled-jsx@5.1.6:
|
| 2920 |
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
|
| 2921 |
engines: {node: '>= 12.0.0'}
|
|
|
|
| 2967 |
thenify@3.3.1:
|
| 2968 |
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
| 2969 |
|
| 2970 |
+
thread-stream@4.0.0:
|
| 2971 |
+
resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==}
|
| 2972 |
+
engines: {node: '>=20'}
|
| 2973 |
+
|
| 2974 |
tiny-invariant@1.3.3:
|
| 2975 |
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
| 2976 |
|
|
|
|
| 3753 |
|
| 3754 |
'@nolyfill/is-core-module@1.0.39': {}
|
| 3755 |
|
| 3756 |
+
'@pinojs/redact@0.4.0': {}
|
| 3757 |
+
|
| 3758 |
'@playwright/test@1.58.2':
|
| 3759 |
dependencies:
|
| 3760 |
playwright: 1.58.2
|
|
|
|
| 4479 |
|
| 4480 |
async-function@1.0.0: {}
|
| 4481 |
|
| 4482 |
+
atomic-sleep@1.0.0: {}
|
| 4483 |
+
|
| 4484 |
autoprefixer@10.4.24(postcss@8.5.6):
|
| 4485 |
dependencies:
|
| 4486 |
browserslist: 4.28.1
|
|
|
|
| 4613 |
|
| 4614 |
color-name@1.1.4: {}
|
| 4615 |
|
| 4616 |
+
colorette@2.0.20: {}
|
| 4617 |
+
|
| 4618 |
commander@4.1.1: {}
|
| 4619 |
|
| 4620 |
concat-map@0.0.1: {}
|
|
|
|
| 4727 |
es-errors: 1.3.0
|
| 4728 |
is-data-view: 1.0.2
|
| 4729 |
|
| 4730 |
+
dateformat@4.6.3: {}
|
| 4731 |
+
|
| 4732 |
debug@3.2.7:
|
| 4733 |
dependencies:
|
| 4734 |
ms: 2.1.3
|
|
|
|
| 5143 |
|
| 5144 |
expect-type@1.3.0: {}
|
| 5145 |
|
| 5146 |
+
fast-copy@4.0.2: {}
|
| 5147 |
+
|
| 5148 |
fast-deep-equal@3.1.3: {}
|
| 5149 |
|
| 5150 |
fast-glob@3.3.1:
|
|
|
|
| 5167 |
|
| 5168 |
fast-levenshtein@2.0.6: {}
|
| 5169 |
|
| 5170 |
+
fast-safe-stringify@2.1.1: {}
|
| 5171 |
+
|
| 5172 |
fastq@1.20.1:
|
| 5173 |
dependencies:
|
| 5174 |
reusify: 1.1.0
|
|
|
|
| 5303 |
dependencies:
|
| 5304 |
function-bind: 1.1.2
|
| 5305 |
|
| 5306 |
+
help-me@5.0.0: {}
|
| 5307 |
+
|
| 5308 |
hermes-estree@0.25.1: {}
|
| 5309 |
|
| 5310 |
hermes-parser@0.25.1:
|
|
|
|
| 5497 |
|
| 5498 |
jiti@1.21.7: {}
|
| 5499 |
|
| 5500 |
+
joycon@3.1.1: {}
|
| 5501 |
+
|
| 5502 |
js-tokens@4.0.0: {}
|
| 5503 |
|
| 5504 |
js-yaml@4.1.1:
|
|
|
|
| 5721 |
define-properties: 1.2.1
|
| 5722 |
es-object-atoms: 1.1.1
|
| 5723 |
|
| 5724 |
+
on-exit-leak-free@2.1.2: {}
|
| 5725 |
+
|
| 5726 |
once@1.4.0:
|
| 5727 |
dependencies:
|
| 5728 |
wrappy: 1.0.2
|
|
|
|
| 5776 |
|
| 5777 |
pify@2.3.0: {}
|
| 5778 |
|
| 5779 |
+
pino-abstract-transport@3.0.0:
|
| 5780 |
+
dependencies:
|
| 5781 |
+
split2: 4.2.0
|
| 5782 |
+
|
| 5783 |
+
pino-pretty@13.1.3:
|
| 5784 |
+
dependencies:
|
| 5785 |
+
colorette: 2.0.20
|
| 5786 |
+
dateformat: 4.6.3
|
| 5787 |
+
fast-copy: 4.0.2
|
| 5788 |
+
fast-safe-stringify: 2.1.1
|
| 5789 |
+
help-me: 5.0.0
|
| 5790 |
+
joycon: 3.1.1
|
| 5791 |
+
minimist: 1.2.8
|
| 5792 |
+
on-exit-leak-free: 2.1.2
|
| 5793 |
+
pino-abstract-transport: 3.0.0
|
| 5794 |
+
pump: 3.0.3
|
| 5795 |
+
secure-json-parse: 4.1.0
|
| 5796 |
+
sonic-boom: 4.2.1
|
| 5797 |
+
strip-json-comments: 5.0.3
|
| 5798 |
+
|
| 5799 |
+
pino-std-serializers@7.1.0: {}
|
| 5800 |
+
|
| 5801 |
+
pino@10.3.1:
|
| 5802 |
+
dependencies:
|
| 5803 |
+
'@pinojs/redact': 0.4.0
|
| 5804 |
+
atomic-sleep: 1.0.0
|
| 5805 |
+
on-exit-leak-free: 2.1.2
|
| 5806 |
+
pino-abstract-transport: 3.0.0
|
| 5807 |
+
pino-std-serializers: 7.1.0
|
| 5808 |
+
process-warning: 5.0.0
|
| 5809 |
+
quick-format-unescaped: 4.0.4
|
| 5810 |
+
real-require: 0.2.0
|
| 5811 |
+
safe-stable-stringify: 2.5.0
|
| 5812 |
+
sonic-boom: 4.2.1
|
| 5813 |
+
thread-stream: 4.0.0
|
| 5814 |
+
|
| 5815 |
pirates@4.0.7: {}
|
| 5816 |
|
| 5817 |
playwright-core@1.58.2: {}
|
|
|
|
| 5890 |
ansi-styles: 5.2.0
|
| 5891 |
react-is: 17.0.2
|
| 5892 |
|
| 5893 |
+
process-warning@5.0.0: {}
|
| 5894 |
+
|
| 5895 |
prop-types@15.8.1:
|
| 5896 |
dependencies:
|
| 5897 |
loose-envify: 1.4.0
|
|
|
|
| 5907 |
|
| 5908 |
queue-microtask@1.2.3: {}
|
| 5909 |
|
| 5910 |
+
quick-format-unescaped@4.0.4: {}
|
| 5911 |
+
|
| 5912 |
rc@1.2.8:
|
| 5913 |
dependencies:
|
| 5914 |
deep-extend: 0.6.0
|
|
|
|
| 5966 |
dependencies:
|
| 5967 |
picomatch: 2.3.1
|
| 5968 |
|
| 5969 |
+
real-require@0.2.0: {}
|
| 5970 |
+
|
| 5971 |
recharts@3.7.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react-is@17.0.2)(react@19.2.4)(redux@5.0.1):
|
| 5972 |
dependencies:
|
| 5973 |
'@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.13)(react@19.2.4)(redux@5.0.1))(react@19.2.4)
|
|
|
|
| 6097 |
es-errors: 1.3.0
|
| 6098 |
is-regex: 1.2.1
|
| 6099 |
|
| 6100 |
+
safe-stable-stringify@2.5.0: {}
|
| 6101 |
+
|
| 6102 |
safer-buffer@2.1.2: {}
|
| 6103 |
|
| 6104 |
saxes@6.0.0:
|
|
|
|
| 6107 |
|
| 6108 |
scheduler@0.27.0: {}
|
| 6109 |
|
| 6110 |
+
secure-json-parse@4.1.0: {}
|
| 6111 |
+
|
| 6112 |
semver@6.3.1: {}
|
| 6113 |
|
| 6114 |
semver@7.7.4: {}
|
|
|
|
| 6211 |
once: 1.4.0
|
| 6212 |
simple-concat: 1.0.1
|
| 6213 |
|
| 6214 |
+
sonic-boom@4.2.1:
|
| 6215 |
+
dependencies:
|
| 6216 |
+
atomic-sleep: 1.0.0
|
| 6217 |
+
|
| 6218 |
source-map-js@1.2.1: {}
|
| 6219 |
|
| 6220 |
+
split2@4.2.0: {}
|
| 6221 |
+
|
| 6222 |
stable-hash@0.0.5: {}
|
| 6223 |
|
| 6224 |
stackback@0.0.2: {}
|
|
|
|
| 6294 |
|
| 6295 |
strip-json-comments@3.1.1: {}
|
| 6296 |
|
| 6297 |
+
strip-json-comments@5.0.3: {}
|
| 6298 |
+
|
| 6299 |
styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4):
|
| 6300 |
dependencies:
|
| 6301 |
client-only: 0.0.1
|
|
|
|
| 6374 |
dependencies:
|
| 6375 |
any-promise: 1.3.0
|
| 6376 |
|
| 6377 |
+
thread-stream@4.0.0:
|
| 6378 |
+
dependencies:
|
| 6379 |
+
real-require: 0.2.0
|
| 6380 |
+
|
| 6381 |
tiny-invariant@1.3.3: {}
|
| 6382 |
|
| 6383 |
tinybench@2.9.0: {}
|
src/app/api/activities/route.ts
CHANGED
|
@@ -38,7 +38,7 @@ async function handleActivitiesRequest(request: NextRequest) {
|
|
| 38 |
const type = searchParams.get('type');
|
| 39 |
const actor = searchParams.get('actor');
|
| 40 |
const entity_type = searchParams.get('entity_type');
|
| 41 |
-
const limit = Math.min(parseInt(searchParams.get('limit') || '50'),
|
| 42 |
const offset = parseInt(searchParams.get('offset') || '0');
|
| 43 |
const since = searchParams.get('since'); // Unix timestamp for real-time updates
|
| 44 |
|
|
|
|
| 38 |
const type = searchParams.get('type');
|
| 39 |
const actor = searchParams.get('actor');
|
| 40 |
const entity_type = searchParams.get('entity_type');
|
| 41 |
+
const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 500);
|
| 42 |
const offset = parseInt(searchParams.get('offset') || '0');
|
| 43 |
const since = searchParams.get('since'); // Unix timestamp for real-time updates
|
| 44 |
|
src/app/api/agents/route.ts
CHANGED
|
@@ -5,6 +5,9 @@ import { getTemplate, buildAgentConfig } from '@/lib/agent-templates';
|
|
| 5 |
import { writeAgentToConfig } from '@/lib/agent-sync';
|
| 6 |
import { logAuditEvent } from '@/lib/db';
|
| 7 |
import { getUserFromRequest, requireRole } from '@/lib/auth';
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
/**
|
| 10 |
* GET /api/agents - List all agents with optional filtering
|
|
@@ -95,7 +98,7 @@ export async function GET(request: NextRequest) {
|
|
| 95 |
limit
|
| 96 |
});
|
| 97 |
} catch (error) {
|
| 98 |
-
|
| 99 |
return NextResponse.json({ error: 'Failed to fetch agents' }, { status: 500 });
|
| 100 |
}
|
| 101 |
}
|
|
@@ -107,9 +110,14 @@ export async function POST(request: NextRequest) {
|
|
| 107 |
const auth = requireRole(request, 'operator');
|
| 108 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 109 |
|
|
|
|
|
|
|
|
|
|
| 110 |
try {
|
| 111 |
const db = getDatabase();
|
| 112 |
-
const
|
|
|
|
|
|
|
| 113 |
|
| 114 |
const {
|
| 115 |
name,
|
|
@@ -125,16 +133,16 @@ export async function POST(request: NextRequest) {
|
|
| 125 |
|
| 126 |
// Resolve template if specified
|
| 127 |
let finalRole = role;
|
| 128 |
-
let finalConfig = config;
|
| 129 |
if (template) {
|
| 130 |
const tpl = getTemplate(template);
|
| 131 |
if (tpl) {
|
| 132 |
-
const builtConfig = buildAgentConfig(tpl, gateway_config || {});
|
| 133 |
-
finalConfig = { ...builtConfig, ...
|
| 134 |
if (!finalRole) finalRole = tpl.config.identity?.theme || tpl.type;
|
| 135 |
}
|
| 136 |
} else if (gateway_config) {
|
| 137 |
-
finalConfig = { ...
|
| 138 |
}
|
| 139 |
|
| 140 |
if (!name || !finalRole) {
|
|
@@ -221,7 +229,7 @@ export async function POST(request: NextRequest) {
|
|
| 221 |
ip_address: ipAddress,
|
| 222 |
});
|
| 223 |
} catch (gwErr: any) {
|
| 224 |
-
|
| 225 |
return NextResponse.json({
|
| 226 |
agent: parsedAgent,
|
| 227 |
warning: `Agent created in MC but gateway write failed: ${gwErr.message}`
|
|
@@ -231,7 +239,7 @@ export async function POST(request: NextRequest) {
|
|
| 231 |
|
| 232 |
return NextResponse.json({ agent: parsedAgent }, { status: 201 });
|
| 233 |
} catch (error) {
|
| 234 |
-
|
| 235 |
return NextResponse.json({ error: 'Failed to create agent' }, { status: 500 });
|
| 236 |
}
|
| 237 |
}
|
|
@@ -243,10 +251,13 @@ export async function PUT(request: NextRequest) {
|
|
| 243 |
const auth = requireRole(request, 'operator');
|
| 244 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 245 |
|
|
|
|
|
|
|
|
|
|
| 246 |
try {
|
| 247 |
const db = getDatabase();
|
| 248 |
const body = await request.json();
|
| 249 |
-
|
| 250 |
// Handle single agent update or bulk updates
|
| 251 |
if (body.name) {
|
| 252 |
// Single agent update
|
|
@@ -343,7 +354,7 @@ export async function PUT(request: NextRequest) {
|
|
| 343 |
return NextResponse.json({ error: 'Agent name is required' }, { status: 400 });
|
| 344 |
}
|
| 345 |
} catch (error) {
|
| 346 |
-
|
| 347 |
return NextResponse.json({ error: 'Failed to update agent' }, { status: 500 });
|
| 348 |
}
|
| 349 |
}
|
|
|
|
| 5 |
import { writeAgentToConfig } from '@/lib/agent-sync';
|
| 6 |
import { logAuditEvent } from '@/lib/db';
|
| 7 |
import { getUserFromRequest, requireRole } from '@/lib/auth';
|
| 8 |
+
import { mutationLimiter } from '@/lib/rate-limit';
|
| 9 |
+
import { logger } from '@/lib/logger';
|
| 10 |
+
import { validateBody, createAgentSchema } from '@/lib/validation';
|
| 11 |
|
| 12 |
/**
|
| 13 |
* GET /api/agents - List all agents with optional filtering
|
|
|
|
| 98 |
limit
|
| 99 |
});
|
| 100 |
} catch (error) {
|
| 101 |
+
logger.error({ err: error }, 'GET /api/agents error');
|
| 102 |
return NextResponse.json({ error: 'Failed to fetch agents' }, { status: 500 });
|
| 103 |
}
|
| 104 |
}
|
|
|
|
| 110 |
const auth = requireRole(request, 'operator');
|
| 111 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 112 |
|
| 113 |
+
const rateCheck = mutationLimiter(request);
|
| 114 |
+
if (rateCheck) return rateCheck;
|
| 115 |
+
|
| 116 |
try {
|
| 117 |
const db = getDatabase();
|
| 118 |
+
const validated = await validateBody(request, createAgentSchema);
|
| 119 |
+
if ('error' in validated) return validated.error;
|
| 120 |
+
const body = validated.data;
|
| 121 |
|
| 122 |
const {
|
| 123 |
name,
|
|
|
|
| 133 |
|
| 134 |
// Resolve template if specified
|
| 135 |
let finalRole = role;
|
| 136 |
+
let finalConfig: Record<string, any> = config as Record<string, any>;
|
| 137 |
if (template) {
|
| 138 |
const tpl = getTemplate(template);
|
| 139 |
if (tpl) {
|
| 140 |
+
const builtConfig = buildAgentConfig(tpl, (gateway_config || {}) as any);
|
| 141 |
+
finalConfig = { ...builtConfig, ...finalConfig };
|
| 142 |
if (!finalRole) finalRole = tpl.config.identity?.theme || tpl.type;
|
| 143 |
}
|
| 144 |
} else if (gateway_config) {
|
| 145 |
+
finalConfig = { ...finalConfig, ...(gateway_config as Record<string, any>) };
|
| 146 |
}
|
| 147 |
|
| 148 |
if (!name || !finalRole) {
|
|
|
|
| 229 |
ip_address: ipAddress,
|
| 230 |
});
|
| 231 |
} catch (gwErr: any) {
|
| 232 |
+
logger.error({ err: gwErr }, 'Gateway write-back failed');
|
| 233 |
return NextResponse.json({
|
| 234 |
agent: parsedAgent,
|
| 235 |
warning: `Agent created in MC but gateway write failed: ${gwErr.message}`
|
|
|
|
| 239 |
|
| 240 |
return NextResponse.json({ agent: parsedAgent }, { status: 201 });
|
| 241 |
} catch (error) {
|
| 242 |
+
logger.error({ err: error }, 'POST /api/agents error');
|
| 243 |
return NextResponse.json({ error: 'Failed to create agent' }, { status: 500 });
|
| 244 |
}
|
| 245 |
}
|
|
|
|
| 251 |
const auth = requireRole(request, 'operator');
|
| 252 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 253 |
|
| 254 |
+
const rateCheck = mutationLimiter(request);
|
| 255 |
+
if (rateCheck) return rateCheck;
|
| 256 |
+
|
| 257 |
try {
|
| 258 |
const db = getDatabase();
|
| 259 |
const body = await request.json();
|
| 260 |
+
|
| 261 |
// Handle single agent update or bulk updates
|
| 262 |
if (body.name) {
|
| 263 |
// Single agent update
|
|
|
|
| 354 |
return NextResponse.json({ error: 'Agent name is required' }, { status: 400 });
|
| 355 |
}
|
| 356 |
} catch (error) {
|
| 357 |
+
logger.error({ err: error }, 'PUT /api/agents error');
|
| 358 |
return NextResponse.json({ error: 'Failed to update agent' }, { status: 500 });
|
| 359 |
}
|
| 360 |
}
|
src/app/api/alerts/route.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
import { NextRequest, NextResponse } from 'next/server'
|
| 2 |
import { requireRole } from '@/lib/auth'
|
| 3 |
import { getDatabase } from '@/lib/db'
|
|
|
|
|
|
|
| 4 |
|
| 5 |
interface AlertRule {
|
| 6 |
id: number
|
|
@@ -44,6 +46,9 @@ export async function POST(request: NextRequest) {
|
|
| 44 |
const auth = requireRole(request, 'operator')
|
| 45 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 46 |
|
|
|
|
|
|
|
|
|
|
| 47 |
const db = getDatabase()
|
| 48 |
const body = await request.json()
|
| 49 |
|
|
@@ -52,22 +57,15 @@ export async function POST(request: NextRequest) {
|
|
| 52 |
return evaluateRules(db)
|
| 53 |
}
|
| 54 |
|
| 55 |
-
//
|
| 56 |
-
const
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
return NextResponse.json({ error: '
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
const validEntities = ['agent', 'task', 'session', 'activity']
|
| 63 |
-
if (!validEntities.includes(entity_type)) {
|
| 64 |
-
return NextResponse.json({ error: `entity_type must be one of: ${validEntities.join(', ')}` }, { status: 400 })
|
| 65 |
}
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
return NextResponse.json({ error: `condition_operator must be one of: ${validOperators.join(', ')}` }, { status: 400 })
|
| 70 |
-
}
|
| 71 |
|
| 72 |
try {
|
| 73 |
const result = db.prepare(`
|
|
@@ -109,6 +107,9 @@ export async function PUT(request: NextRequest) {
|
|
| 109 |
const auth = requireRole(request, 'operator')
|
| 110 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 111 |
|
|
|
|
|
|
|
|
|
|
| 112 |
const db = getDatabase()
|
| 113 |
const body = await request.json()
|
| 114 |
const { id, ...updates } = body
|
|
@@ -147,6 +148,9 @@ export async function DELETE(request: NextRequest) {
|
|
| 147 |
const auth = requireRole(request, 'admin')
|
| 148 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 149 |
|
|
|
|
|
|
|
|
|
|
| 150 |
const db = getDatabase()
|
| 151 |
const body = await request.json()
|
| 152 |
const { id } = body
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from 'next/server'
|
| 2 |
import { requireRole } from '@/lib/auth'
|
| 3 |
import { getDatabase } from '@/lib/db'
|
| 4 |
+
import { mutationLimiter } from '@/lib/rate-limit'
|
| 5 |
+
import { createAlertSchema } from '@/lib/validation'
|
| 6 |
|
| 7 |
interface AlertRule {
|
| 8 |
id: number
|
|
|
|
| 46 |
const auth = requireRole(request, 'operator')
|
| 47 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 48 |
|
| 49 |
+
const rateCheck = mutationLimiter(request)
|
| 50 |
+
if (rateCheck) return rateCheck
|
| 51 |
+
|
| 52 |
const db = getDatabase()
|
| 53 |
const body = await request.json()
|
| 54 |
|
|
|
|
| 57 |
return evaluateRules(db)
|
| 58 |
}
|
| 59 |
|
| 60 |
+
// Validate for create
|
| 61 |
+
const parseResult = createAlertSchema.safeParse(body)
|
| 62 |
+
if (!parseResult.success) {
|
| 63 |
+
const messages = parseResult.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`)
|
| 64 |
+
return NextResponse.json({ error: 'Validation failed', details: messages }, { status: 400 })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
+
// Create new rule
|
| 68 |
+
const { name, description, entity_type, condition_field, condition_operator, condition_value, action_type, action_config, cooldown_minutes } = parseResult.data
|
|
|
|
|
|
|
| 69 |
|
| 70 |
try {
|
| 71 |
const result = db.prepare(`
|
|
|
|
| 107 |
const auth = requireRole(request, 'operator')
|
| 108 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 109 |
|
| 110 |
+
const rateCheck = mutationLimiter(request)
|
| 111 |
+
if (rateCheck) return rateCheck
|
| 112 |
+
|
| 113 |
const db = getDatabase()
|
| 114 |
const body = await request.json()
|
| 115 |
const { id, ...updates } = body
|
|
|
|
| 148 |
const auth = requireRole(request, 'admin')
|
| 149 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 150 |
|
| 151 |
+
const rateCheck = mutationLimiter(request)
|
| 152 |
+
if (rateCheck) return rateCheck
|
| 153 |
+
|
| 154 |
const db = getDatabase()
|
| 155 |
const body = await request.json()
|
| 156 |
const { id } = body
|
src/app/api/audit/route.ts
CHANGED
|
@@ -17,7 +17,7 @@ export async function GET(request: NextRequest) {
|
|
| 17 |
const { searchParams } = new URL(request.url)
|
| 18 |
const action = searchParams.get('action')
|
| 19 |
const actor = searchParams.get('actor')
|
| 20 |
-
const limit = Math.min(parseInt(searchParams.get('limit') || '
|
| 21 |
const offset = parseInt(searchParams.get('offset') || '0')
|
| 22 |
const since = searchParams.get('since')
|
| 23 |
const until = searchParams.get('until')
|
|
|
|
| 17 |
const { searchParams } = new URL(request.url)
|
| 18 |
const action = searchParams.get('action')
|
| 19 |
const actor = searchParams.get('actor')
|
| 20 |
+
const limit = Math.min(parseInt(searchParams.get('limit') || '1000'), 10000)
|
| 21 |
const offset = parseInt(searchParams.get('offset') || '0')
|
| 22 |
const since = searchParams.get('since')
|
| 23 |
const until = searchParams.get('until')
|
src/app/api/auth/login/route.ts
CHANGED
|
@@ -2,27 +2,13 @@ import { NextResponse } from 'next/server'
|
|
| 2 |
import { authenticateUser, createSession } from '@/lib/auth'
|
| 3 |
import { logAuditEvent } from '@/lib/db'
|
| 4 |
import { getMcSessionCookieOptions } from '@/lib/session-cookie'
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
const loginAttempts = new Map<string, { count: number; resetAt: number }>()
|
| 8 |
-
|
| 9 |
-
function checkRateLimit(ip: string): boolean {
|
| 10 |
-
const now = Date.now()
|
| 11 |
-
const entry = loginAttempts.get(ip)
|
| 12 |
-
if (!entry || now > entry.resetAt) {
|
| 13 |
-
loginAttempts.set(ip, { count: 1, resetAt: now + 60_000 })
|
| 14 |
-
return true
|
| 15 |
-
}
|
| 16 |
-
entry.count++
|
| 17 |
-
return entry.count <= 5
|
| 18 |
-
}
|
| 19 |
|
| 20 |
export async function POST(request: Request) {
|
| 21 |
try {
|
| 22 |
-
const
|
| 23 |
-
if (
|
| 24 |
-
return NextResponse.json({ error: 'Too many login attempts. Try again in a minute.' }, { status: 429 })
|
| 25 |
-
}
|
| 26 |
|
| 27 |
const { username, password } = await request.json()
|
| 28 |
|
|
@@ -61,7 +47,7 @@ export async function POST(request: Request) {
|
|
| 61 |
|
| 62 |
return response
|
| 63 |
} catch (error) {
|
| 64 |
-
|
| 65 |
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
| 66 |
}
|
| 67 |
}
|
|
|
|
| 2 |
import { authenticateUser, createSession } from '@/lib/auth'
|
| 3 |
import { logAuditEvent } from '@/lib/db'
|
| 4 |
import { getMcSessionCookieOptions } from '@/lib/session-cookie'
|
| 5 |
+
import { loginLimiter } from '@/lib/rate-limit'
|
| 6 |
+
import { logger } from '@/lib/logger'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
export async function POST(request: Request) {
|
| 9 |
try {
|
| 10 |
+
const rateCheck = loginLimiter(request)
|
| 11 |
+
if (rateCheck) return rateCheck
|
|
|
|
|
|
|
| 12 |
|
| 13 |
const { username, password } = await request.json()
|
| 14 |
|
|
|
|
| 47 |
|
| 48 |
return response
|
| 49 |
} catch (error) {
|
| 50 |
+
logger.error({ err: error }, 'Login error')
|
| 51 |
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
| 52 |
}
|
| 53 |
}
|
src/app/api/export/route.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import { NextRequest, NextResponse } from 'next/server'
|
| 2 |
import { requireRole } from '@/lib/auth'
|
| 3 |
import { getDatabase, logAuditEvent } from '@/lib/db'
|
|
|
|
| 4 |
|
| 5 |
/**
|
| 6 |
* GET /api/export?type=audit|tasks|activities|pipelines&format=csv|json&since=UNIX&until=UNIX
|
|
@@ -10,6 +11,9 @@ export async function GET(request: NextRequest) {
|
|
| 10 |
const auth = requireRole(request, 'admin')
|
| 11 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
const { searchParams } = new URL(request.url)
|
| 14 |
const type = searchParams.get('type')
|
| 15 |
const format = searchParams.get('format') || 'csv'
|
|
@@ -38,31 +42,35 @@ export async function GET(request: NextRequest) {
|
|
| 38 |
|
| 39 |
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
let rows: any[] = []
|
| 42 |
let headers: string[] = []
|
| 43 |
let filename = ''
|
| 44 |
|
| 45 |
switch (type) {
|
| 46 |
case 'audit': {
|
| 47 |
-
rows = db.prepare(`SELECT * FROM audit_log ${where} ORDER BY created_at DESC`).all(...params)
|
| 48 |
headers = ['id', 'action', 'actor', 'actor_id', 'target_type', 'target_id', 'detail', 'ip_address', 'user_agent', 'created_at']
|
| 49 |
filename = 'audit-log'
|
| 50 |
break
|
| 51 |
}
|
| 52 |
case 'tasks': {
|
| 53 |
-
rows = db.prepare(`SELECT * FROM tasks ${where} ORDER BY created_at DESC`).all(...params)
|
| 54 |
headers = ['id', 'title', 'description', 'status', 'priority', 'assigned_to', 'created_by', 'created_at', 'updated_at', 'due_date', 'estimated_hours', 'actual_hours', 'tags']
|
| 55 |
filename = 'tasks'
|
| 56 |
break
|
| 57 |
}
|
| 58 |
case 'activities': {
|
| 59 |
-
rows = db.prepare(`SELECT * FROM activities ${where} ORDER BY created_at DESC`).all(...params)
|
| 60 |
headers = ['id', 'type', 'entity_type', 'entity_id', 'actor', 'description', 'data', 'created_at']
|
| 61 |
filename = 'activities'
|
| 62 |
break
|
| 63 |
}
|
| 64 |
case 'pipelines': {
|
| 65 |
-
rows = db.prepare(`SELECT pr.*, wp.name as pipeline_name FROM pipeline_runs pr LEFT JOIN workflow_pipelines wp ON pr.pipeline_id = wp.id ${where ? where.replace('created_at', 'pr.created_at') : ''} ORDER BY pr.created_at DESC`).all(...params)
|
| 66 |
headers = ['id', 'pipeline_id', 'pipeline_name', 'status', 'current_step', 'steps_snapshot', 'started_at', 'completed_at', 'triggered_by', 'created_at']
|
| 67 |
filename = 'pipeline-runs'
|
| 68 |
break
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from 'next/server'
|
| 2 |
import { requireRole } from '@/lib/auth'
|
| 3 |
import { getDatabase, logAuditEvent } from '@/lib/db'
|
| 4 |
+
import { heavyLimiter } from '@/lib/rate-limit'
|
| 5 |
|
| 6 |
/**
|
| 7 |
* GET /api/export?type=audit|tasks|activities|pipelines&format=csv|json&since=UNIX&until=UNIX
|
|
|
|
| 11 |
const auth = requireRole(request, 'admin')
|
| 12 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 13 |
|
| 14 |
+
const rateCheck = heavyLimiter(request)
|
| 15 |
+
if (rateCheck) return rateCheck
|
| 16 |
+
|
| 17 |
const { searchParams } = new URL(request.url)
|
| 18 |
const type = searchParams.get('type')
|
| 19 |
const format = searchParams.get('format') || 'csv'
|
|
|
|
| 42 |
|
| 43 |
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
| 44 |
|
| 45 |
+
const requestedLimit = parseInt(searchParams.get('limit') || '10000')
|
| 46 |
+
const maxLimit = 50000
|
| 47 |
+
const limit = Math.min(requestedLimit, maxLimit)
|
| 48 |
+
|
| 49 |
let rows: any[] = []
|
| 50 |
let headers: string[] = []
|
| 51 |
let filename = ''
|
| 52 |
|
| 53 |
switch (type) {
|
| 54 |
case 'audit': {
|
| 55 |
+
rows = db.prepare(`SELECT * FROM audit_log ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit)
|
| 56 |
headers = ['id', 'action', 'actor', 'actor_id', 'target_type', 'target_id', 'detail', 'ip_address', 'user_agent', 'created_at']
|
| 57 |
filename = 'audit-log'
|
| 58 |
break
|
| 59 |
}
|
| 60 |
case 'tasks': {
|
| 61 |
+
rows = db.prepare(`SELECT * FROM tasks ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit)
|
| 62 |
headers = ['id', 'title', 'description', 'status', 'priority', 'assigned_to', 'created_by', 'created_at', 'updated_at', 'due_date', 'estimated_hours', 'actual_hours', 'tags']
|
| 63 |
filename = 'tasks'
|
| 64 |
break
|
| 65 |
}
|
| 66 |
case 'activities': {
|
| 67 |
+
rows = db.prepare(`SELECT * FROM activities ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit)
|
| 68 |
headers = ['id', 'type', 'entity_type', 'entity_id', 'actor', 'description', 'data', 'created_at']
|
| 69 |
filename = 'activities'
|
| 70 |
break
|
| 71 |
}
|
| 72 |
case 'pipelines': {
|
| 73 |
+
rows = db.prepare(`SELECT pr.*, wp.name as pipeline_name FROM pipeline_runs pr LEFT JOIN workflow_pipelines wp ON pr.pipeline_id = wp.id ${where ? where.replace('created_at', 'pr.created_at') : ''} ORDER BY pr.created_at DESC LIMIT ?`).all(...params, limit)
|
| 74 |
headers = ['id', 'pipeline_id', 'pipeline_name', 'status', 'current_step', 'steps_snapshot', 'started_at', 'completed_at', 'triggered_by', 'created_at']
|
| 75 |
filename = 'pipeline-runs'
|
| 76 |
break
|
src/app/api/notifications/route.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
import { getDatabase, Notification } from '@/lib/db';
|
| 3 |
import { requireRole } from '@/lib/auth';
|
|
|
|
| 4 |
|
| 5 |
/**
|
| 6 |
* GET /api/notifications - Get notifications for a specific recipient
|
|
@@ -18,7 +19,7 @@ export async function GET(request: NextRequest) {
|
|
| 18 |
const recipient = searchParams.get('recipient');
|
| 19 |
const unread_only = searchParams.get('unread_only') === 'true';
|
| 20 |
const type = searchParams.get('type');
|
| 21 |
-
const limit = Math.min(parseInt(searchParams.get('limit') || '50'),
|
| 22 |
const offset = parseInt(searchParams.get('offset') || '0');
|
| 23 |
|
| 24 |
if (!recipient) {
|
|
@@ -138,6 +139,9 @@ export async function PUT(request: NextRequest) {
|
|
| 138 |
const auth = requireRole(request, 'operator');
|
| 139 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 140 |
|
|
|
|
|
|
|
|
|
|
| 141 |
try {
|
| 142 |
const db = getDatabase();
|
| 143 |
const body = await request.json();
|
|
@@ -193,6 +197,9 @@ export async function DELETE(request: NextRequest) {
|
|
| 193 |
const auth = requireRole(request, 'admin');
|
| 194 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 195 |
|
|
|
|
|
|
|
|
|
|
| 196 |
try {
|
| 197 |
const db = getDatabase();
|
| 198 |
const body = await request.json();
|
|
@@ -244,6 +251,9 @@ export async function POST(request: NextRequest) {
|
|
| 244 |
const auth = requireRole(request, 'operator');
|
| 245 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 246 |
|
|
|
|
|
|
|
|
|
|
| 247 |
try {
|
| 248 |
const db = getDatabase();
|
| 249 |
const body = await request.json();
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from 'next/server';
|
| 2 |
import { getDatabase, Notification } from '@/lib/db';
|
| 3 |
import { requireRole } from '@/lib/auth';
|
| 4 |
+
import { mutationLimiter } from '@/lib/rate-limit';
|
| 5 |
|
| 6 |
/**
|
| 7 |
* GET /api/notifications - Get notifications for a specific recipient
|
|
|
|
| 19 |
const recipient = searchParams.get('recipient');
|
| 20 |
const unread_only = searchParams.get('unread_only') === 'true';
|
| 21 |
const type = searchParams.get('type');
|
| 22 |
+
const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 500);
|
| 23 |
const offset = parseInt(searchParams.get('offset') || '0');
|
| 24 |
|
| 25 |
if (!recipient) {
|
|
|
|
| 139 |
const auth = requireRole(request, 'operator');
|
| 140 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 141 |
|
| 142 |
+
const rateCheck = mutationLimiter(request);
|
| 143 |
+
if (rateCheck) return rateCheck;
|
| 144 |
+
|
| 145 |
try {
|
| 146 |
const db = getDatabase();
|
| 147 |
const body = await request.json();
|
|
|
|
| 197 |
const auth = requireRole(request, 'admin');
|
| 198 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 199 |
|
| 200 |
+
const rateCheck = mutationLimiter(request);
|
| 201 |
+
if (rateCheck) return rateCheck;
|
| 202 |
+
|
| 203 |
try {
|
| 204 |
const db = getDatabase();
|
| 205 |
const body = await request.json();
|
|
|
|
| 251 |
const auth = requireRole(request, 'operator');
|
| 252 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 253 |
|
| 254 |
+
const rateCheck = mutationLimiter(request);
|
| 255 |
+
if (rateCheck) return rateCheck;
|
| 256 |
+
|
| 257 |
try {
|
| 258 |
const db = getDatabase();
|
| 259 |
const body = await request.json();
|
src/app/api/sessions/[id]/control/route.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from 'next/server'
|
| 2 |
+
import { requireRole } from '@/lib/auth'
|
| 3 |
+
import { runClawdbot } from '@/lib/command'
|
| 4 |
+
import { db_helpers } from '@/lib/db'
|
| 5 |
+
|
| 6 |
+
export async function POST(
|
| 7 |
+
request: NextRequest,
|
| 8 |
+
{ params }: { params: Promise<{ id: string }> }
|
| 9 |
+
) {
|
| 10 |
+
const auth = requireRole(request, 'operator')
|
| 11 |
+
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 12 |
+
|
| 13 |
+
try {
|
| 14 |
+
const { id } = await params
|
| 15 |
+
const { action } = await request.json()
|
| 16 |
+
|
| 17 |
+
if (!['monitor', 'pause', 'terminate'].includes(action)) {
|
| 18 |
+
return NextResponse.json(
|
| 19 |
+
{ error: 'Invalid action. Must be: monitor, pause, terminate' },
|
| 20 |
+
{ status: 400 }
|
| 21 |
+
)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
let result
|
| 25 |
+
if (action === 'terminate') {
|
| 26 |
+
result = await runClawdbot(
|
| 27 |
+
['-c', `sessions_kill("${id}")`],
|
| 28 |
+
{ timeoutMs: 10000 }
|
| 29 |
+
)
|
| 30 |
+
} else {
|
| 31 |
+
const message = action === 'monitor'
|
| 32 |
+
? JSON.stringify({ type: 'control', action: 'monitor' })
|
| 33 |
+
: JSON.stringify({ type: 'control', action: 'pause' })
|
| 34 |
+
result = await runClawdbot(
|
| 35 |
+
['-c', `sessions_send("${id}", ${JSON.stringify(message)})`],
|
| 36 |
+
{ timeoutMs: 10000 }
|
| 37 |
+
)
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
db_helpers.logActivity(
|
| 41 |
+
'session_control',
|
| 42 |
+
'session',
|
| 43 |
+
0,
|
| 44 |
+
auth.user.username,
|
| 45 |
+
`Session ${action}: ${id}`,
|
| 46 |
+
{ session_key: id, action }
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
return NextResponse.json({
|
| 50 |
+
success: true,
|
| 51 |
+
action,
|
| 52 |
+
session: id,
|
| 53 |
+
stdout: result.stdout.trim(),
|
| 54 |
+
})
|
| 55 |
+
} catch (error: any) {
|
| 56 |
+
return NextResponse.json(
|
| 57 |
+
{ error: error.message || 'Session control failed' },
|
| 58 |
+
{ status: 500 }
|
| 59 |
+
)
|
| 60 |
+
}
|
| 61 |
+
}
|
src/app/api/settings/route.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
|
| 2 |
import { requireRole } from '@/lib/auth'
|
| 3 |
import { getDatabase, logAuditEvent } from '@/lib/db'
|
| 4 |
import { config } from '@/lib/config'
|
|
|
|
| 5 |
|
| 6 |
interface SettingRow {
|
| 7 |
key: string
|
|
@@ -101,6 +102,9 @@ export async function PUT(request: NextRequest) {
|
|
| 101 |
const auth = requireRole(request, 'admin')
|
| 102 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 103 |
|
|
|
|
|
|
|
|
|
|
| 104 |
const body = await request.json().catch(() => null)
|
| 105 |
if (!body?.settings || typeof body.settings !== 'object') {
|
| 106 |
return NextResponse.json({ error: 'settings object required' }, { status: 400 })
|
|
@@ -157,6 +161,9 @@ export async function DELETE(request: NextRequest) {
|
|
| 157 |
const auth = requireRole(request, 'admin')
|
| 158 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 159 |
|
|
|
|
|
|
|
|
|
|
| 160 |
let body: any
|
| 161 |
try { body = await request.json() } catch { return NextResponse.json({ error: 'Request body required' }, { status: 400 }) }
|
| 162 |
const key = body.key
|
|
|
|
| 2 |
import { requireRole } from '@/lib/auth'
|
| 3 |
import { getDatabase, logAuditEvent } from '@/lib/db'
|
| 4 |
import { config } from '@/lib/config'
|
| 5 |
+
import { mutationLimiter } from '@/lib/rate-limit'
|
| 6 |
|
| 7 |
interface SettingRow {
|
| 8 |
key: string
|
|
|
|
| 102 |
const auth = requireRole(request, 'admin')
|
| 103 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 104 |
|
| 105 |
+
const rateCheck = mutationLimiter(request)
|
| 106 |
+
if (rateCheck) return rateCheck
|
| 107 |
+
|
| 108 |
const body = await request.json().catch(() => null)
|
| 109 |
if (!body?.settings || typeof body.settings !== 'object') {
|
| 110 |
return NextResponse.json({ error: 'settings object required' }, { status: 400 })
|
|
|
|
| 161 |
const auth = requireRole(request, 'admin')
|
| 162 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 163 |
|
| 164 |
+
const rateCheck = mutationLimiter(request)
|
| 165 |
+
if (rateCheck) return rateCheck
|
| 166 |
+
|
| 167 |
let body: any
|
| 168 |
try { body = await request.json() } catch { return NextResponse.json({ error: 'Request body required' }, { status: 400 }) }
|
| 169 |
const key = body.key
|
src/app/api/spawn/route.ts
CHANGED
|
@@ -4,11 +4,16 @@ import { requireRole } from '@/lib/auth'
|
|
| 4 |
import { config } from '@/lib/config'
|
| 5 |
import { readdir, readFile, stat } from 'fs/promises'
|
| 6 |
import { join } from 'path'
|
|
|
|
|
|
|
| 7 |
|
| 8 |
export async function POST(request: NextRequest) {
|
| 9 |
const auth = requireRole(request, 'operator')
|
| 10 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 11 |
|
|
|
|
|
|
|
|
|
|
| 12 |
try {
|
| 13 |
const { task, model, label, timeoutSeconds } = await request.json()
|
| 14 |
|
|
@@ -57,7 +62,7 @@ export async function POST(request: NextRequest) {
|
|
| 57 |
sessionInfo = sessionMatch[1]
|
| 58 |
}
|
| 59 |
} catch (parseError) {
|
| 60 |
-
|
| 61 |
}
|
| 62 |
|
| 63 |
return NextResponse.json({
|
|
@@ -74,7 +79,7 @@ export async function POST(request: NextRequest) {
|
|
| 74 |
})
|
| 75 |
|
| 76 |
} catch (execError: any) {
|
| 77 |
-
|
| 78 |
|
| 79 |
return NextResponse.json({
|
| 80 |
success: false,
|
|
@@ -89,7 +94,7 @@ export async function POST(request: NextRequest) {
|
|
| 89 |
}
|
| 90 |
|
| 91 |
} catch (error) {
|
| 92 |
-
|
| 93 |
return NextResponse.json(
|
| 94 |
{ error: 'Internal server error' },
|
| 95 |
{ status: 500 }
|
|
@@ -173,7 +178,7 @@ export async function GET(request: NextRequest) {
|
|
| 173 |
}
|
| 174 |
|
| 175 |
} catch (error) {
|
| 176 |
-
|
| 177 |
return NextResponse.json(
|
| 178 |
{ error: 'Internal server error' },
|
| 179 |
{ status: 500 }
|
|
|
|
| 4 |
import { config } from '@/lib/config'
|
| 5 |
import { readdir, readFile, stat } from 'fs/promises'
|
| 6 |
import { join } from 'path'
|
| 7 |
+
import { heavyLimiter } from '@/lib/rate-limit'
|
| 8 |
+
import { logger } from '@/lib/logger'
|
| 9 |
|
| 10 |
export async function POST(request: NextRequest) {
|
| 11 |
const auth = requireRole(request, 'operator')
|
| 12 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 13 |
|
| 14 |
+
const rateCheck = heavyLimiter(request)
|
| 15 |
+
if (rateCheck) return rateCheck
|
| 16 |
+
|
| 17 |
try {
|
| 18 |
const { task, model, label, timeoutSeconds } = await request.json()
|
| 19 |
|
|
|
|
| 62 |
sessionInfo = sessionMatch[1]
|
| 63 |
}
|
| 64 |
} catch (parseError) {
|
| 65 |
+
logger.error({ err: parseError }, 'Failed to parse session info')
|
| 66 |
}
|
| 67 |
|
| 68 |
return NextResponse.json({
|
|
|
|
| 79 |
})
|
| 80 |
|
| 81 |
} catch (execError: any) {
|
| 82 |
+
logger.error({ err: execError }, 'Spawn execution error')
|
| 83 |
|
| 84 |
return NextResponse.json({
|
| 85 |
success: false,
|
|
|
|
| 94 |
}
|
| 95 |
|
| 96 |
} catch (error) {
|
| 97 |
+
logger.error({ err: error }, 'Spawn API error')
|
| 98 |
return NextResponse.json(
|
| 99 |
{ error: 'Internal server error' },
|
| 100 |
{ status: 500 }
|
|
|
|
| 178 |
}
|
| 179 |
|
| 180 |
} catch (error) {
|
| 181 |
+
logger.error({ err: error }, 'Spawn history API error')
|
| 182 |
return NextResponse.json(
|
| 183 |
{ error: 'Internal server error' },
|
| 184 |
{ status: 500 }
|
src/app/api/status/route.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { config } from '@/lib/config'
|
|
| 6 |
import { getDatabase } from '@/lib/db'
|
| 7 |
import { getAllGatewaySessions, getAgentLiveStatuses } from '@/lib/sessions'
|
| 8 |
import { requireRole } from '@/lib/auth'
|
|
|
|
| 9 |
|
| 10 |
export async function GET(request: NextRequest) {
|
| 11 |
const auth = requireRole(request, 'viewer')
|
|
@@ -340,17 +341,8 @@ async function getGatewayStatus() {
|
|
| 340 |
|
| 341 |
async function getAvailableModels() {
|
| 342 |
// This would typically query the gateway or config files
|
| 343 |
-
//
|
| 344 |
-
const models = [
|
| 345 |
-
{ alias: 'haiku', name: 'anthropic/claude-3-5-haiku-latest', provider: 'anthropic', description: 'Ultra-cheap, simple tasks', costPer1k: 0.25 },
|
| 346 |
-
{ alias: 'sonnet', name: 'anthropic/claude-sonnet-4-20250514', provider: 'anthropic', description: 'Standard workhorse', costPer1k: 3.0 },
|
| 347 |
-
{ alias: 'opus', name: 'anthropic/claude-opus-4-5', provider: 'anthropic', description: 'Premium quality', costPer1k: 15.0 },
|
| 348 |
-
{ alias: 'deepseek', name: 'ollama/deepseek-r1:14b', provider: 'ollama', description: 'Local reasoning (free)', costPer1k: 0.0 },
|
| 349 |
-
{ alias: 'groq-fast', name: 'groq/llama-3.1-8b-instant', provider: 'groq', description: '840 tok/s, ultra fast', costPer1k: 0.05 },
|
| 350 |
-
{ alias: 'groq', name: 'groq/llama-3.3-70b-versatile', provider: 'groq', description: 'Fast + quality balance', costPer1k: 0.59 },
|
| 351 |
-
{ alias: 'kimi', name: 'moonshot/kimi-k2.5', provider: 'moonshot', description: 'Alternative provider', costPer1k: 1.0 },
|
| 352 |
-
{ alias: 'minimax', name: 'minimax/minimax-m2.1', provider: 'minimax', description: 'Cost-effective (1/10th price), strong coding', costPer1k: 0.3 },
|
| 353 |
-
]
|
| 354 |
|
| 355 |
try {
|
| 356 |
// Check which Ollama models are available locally
|
|
|
|
| 6 |
import { getDatabase } from '@/lib/db'
|
| 7 |
import { getAllGatewaySessions, getAgentLiveStatuses } from '@/lib/sessions'
|
| 8 |
import { requireRole } from '@/lib/auth'
|
| 9 |
+
import { MODEL_CATALOG } from '@/lib/models'
|
| 10 |
|
| 11 |
export async function GET(request: NextRequest) {
|
| 12 |
const auth = requireRole(request, 'viewer')
|
|
|
|
| 341 |
|
| 342 |
async function getAvailableModels() {
|
| 343 |
// This would typically query the gateway or config files
|
| 344 |
+
// Model catalog is the single source of truth
|
| 345 |
+
const models = [...MODEL_CATALOG]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
|
| 347 |
try {
|
| 348 |
// Check which Ollama models are available locally
|
src/app/api/tasks/[id]/route.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
| 2 |
import { getDatabase, Task, db_helpers } from '@/lib/db';
|
| 3 |
import { eventBus } from '@/lib/event-bus';
|
| 4 |
import { getUserFromRequest, requireRole } from '@/lib/auth';
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
function hasAegisApproval(db: ReturnType<typeof getDatabase>, taskId: number): boolean {
|
| 7 |
const review = db.prepare(`
|
|
@@ -48,7 +51,7 @@ export async function GET(
|
|
| 48 |
|
| 49 |
return NextResponse.json({ task: taskWithParsedData });
|
| 50 |
} catch (error) {
|
| 51 |
-
|
| 52 |
return NextResponse.json({ error: 'Failed to fetch task' }, { status: 500 });
|
| 53 |
}
|
| 54 |
}
|
|
@@ -63,11 +66,16 @@ export async function PUT(
|
|
| 63 |
const auth = requireRole(request, 'operator');
|
| 64 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 65 |
|
|
|
|
|
|
|
|
|
|
| 66 |
try {
|
| 67 |
const db = getDatabase();
|
| 68 |
const resolvedParams = await params;
|
| 69 |
const taskId = parseInt(resolvedParams.id);
|
| 70 |
-
const
|
|
|
|
|
|
|
| 71 |
|
| 72 |
if (isNaN(taskId)) {
|
| 73 |
return NextResponse.json({ error: 'Invalid task ID' }, { status: 400 });
|
|
@@ -240,7 +248,7 @@ export async function PUT(
|
|
| 240 |
|
| 241 |
return NextResponse.json({ task: parsedTask });
|
| 242 |
} catch (error) {
|
| 243 |
-
|
| 244 |
return NextResponse.json({ error: 'Failed to update task' }, { status: 500 });
|
| 245 |
}
|
| 246 |
}
|
|
@@ -255,6 +263,9 @@ export async function DELETE(
|
|
| 255 |
const auth = requireRole(request, 'operator');
|
| 256 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 257 |
|
|
|
|
|
|
|
|
|
|
| 258 |
try {
|
| 259 |
const db = getDatabase();
|
| 260 |
const resolvedParams = await params;
|
|
@@ -294,7 +305,7 @@ export async function DELETE(
|
|
| 294 |
|
| 295 |
return NextResponse.json({ success: true });
|
| 296 |
} catch (error) {
|
| 297 |
-
|
| 298 |
return NextResponse.json({ error: 'Failed to delete task' }, { status: 500 });
|
| 299 |
}
|
| 300 |
}
|
|
|
|
| 2 |
import { getDatabase, Task, db_helpers } from '@/lib/db';
|
| 3 |
import { eventBus } from '@/lib/event-bus';
|
| 4 |
import { getUserFromRequest, requireRole } from '@/lib/auth';
|
| 5 |
+
import { mutationLimiter } from '@/lib/rate-limit';
|
| 6 |
+
import { logger } from '@/lib/logger';
|
| 7 |
+
import { validateBody, updateTaskSchema } from '@/lib/validation';
|
| 8 |
|
| 9 |
function hasAegisApproval(db: ReturnType<typeof getDatabase>, taskId: number): boolean {
|
| 10 |
const review = db.prepare(`
|
|
|
|
| 51 |
|
| 52 |
return NextResponse.json({ task: taskWithParsedData });
|
| 53 |
} catch (error) {
|
| 54 |
+
logger.error({ err: error }, 'GET /api/tasks/[id] error');
|
| 55 |
return NextResponse.json({ error: 'Failed to fetch task' }, { status: 500 });
|
| 56 |
}
|
| 57 |
}
|
|
|
|
| 66 |
const auth = requireRole(request, 'operator');
|
| 67 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 68 |
|
| 69 |
+
const rateCheck = mutationLimiter(request);
|
| 70 |
+
if (rateCheck) return rateCheck;
|
| 71 |
+
|
| 72 |
try {
|
| 73 |
const db = getDatabase();
|
| 74 |
const resolvedParams = await params;
|
| 75 |
const taskId = parseInt(resolvedParams.id);
|
| 76 |
+
const validated = await validateBody(request, updateTaskSchema);
|
| 77 |
+
if ('error' in validated) return validated.error;
|
| 78 |
+
const body = validated.data;
|
| 79 |
|
| 80 |
if (isNaN(taskId)) {
|
| 81 |
return NextResponse.json({ error: 'Invalid task ID' }, { status: 400 });
|
|
|
|
| 248 |
|
| 249 |
return NextResponse.json({ task: parsedTask });
|
| 250 |
} catch (error) {
|
| 251 |
+
logger.error({ err: error }, 'PUT /api/tasks/[id] error');
|
| 252 |
return NextResponse.json({ error: 'Failed to update task' }, { status: 500 });
|
| 253 |
}
|
| 254 |
}
|
|
|
|
| 263 |
const auth = requireRole(request, 'operator');
|
| 264 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 265 |
|
| 266 |
+
const rateCheck = mutationLimiter(request);
|
| 267 |
+
if (rateCheck) return rateCheck;
|
| 268 |
+
|
| 269 |
try {
|
| 270 |
const db = getDatabase();
|
| 271 |
const resolvedParams = await params;
|
|
|
|
| 305 |
|
| 306 |
return NextResponse.json({ success: true });
|
| 307 |
} catch (error) {
|
| 308 |
+
logger.error({ err: error }, 'DELETE /api/tasks/[id] error');
|
| 309 |
return NextResponse.json({ error: 'Failed to delete task' }, { status: 500 });
|
| 310 |
}
|
| 311 |
}
|
src/app/api/tasks/route.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
| 2 |
import { getDatabase, Task, db_helpers } from '@/lib/db';
|
| 3 |
import { eventBus } from '@/lib/event-bus';
|
| 4 |
import { requireRole } from '@/lib/auth';
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
function hasAegisApproval(db: ReturnType<typeof getDatabase>, taskId: number): boolean {
|
| 7 |
const review = db.prepare(`
|
|
@@ -83,7 +86,7 @@ export async function GET(request: NextRequest) {
|
|
| 83 |
|
| 84 |
return NextResponse.json({ tasks: tasksWithParsedData, total: countRow.total, page: Math.floor(offset / limit) + 1, limit });
|
| 85 |
} catch (error) {
|
| 86 |
-
|
| 87 |
return NextResponse.json({ error: 'Failed to fetch tasks' }, { status: 500 });
|
| 88 |
}
|
| 89 |
}
|
|
@@ -95,9 +98,14 @@ export async function POST(request: NextRequest) {
|
|
| 95 |
const auth = requireRole(request, 'operator');
|
| 96 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 97 |
|
|
|
|
|
|
|
|
|
|
| 98 |
try {
|
| 99 |
const db = getDatabase();
|
| 100 |
-
const
|
|
|
|
|
|
|
| 101 |
|
| 102 |
const user = auth.user
|
| 103 |
const {
|
|
@@ -113,10 +121,6 @@ export async function POST(request: NextRequest) {
|
|
| 113 |
metadata = {}
|
| 114 |
} = body;
|
| 115 |
|
| 116 |
-
if (!title) {
|
| 117 |
-
return NextResponse.json({ error: 'Title is required' }, { status: 400 });
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
// Check for duplicate title
|
| 121 |
const existingTask = db.prepare('SELECT id FROM tasks WHERE title = ?').get(title);
|
| 122 |
if (existingTask) {
|
|
@@ -187,7 +191,7 @@ export async function POST(request: NextRequest) {
|
|
| 187 |
|
| 188 |
return NextResponse.json({ task: parsedTask }, { status: 201 });
|
| 189 |
} catch (error) {
|
| 190 |
-
|
| 191 |
return NextResponse.json({ error: 'Failed to create task' }, { status: 500 });
|
| 192 |
}
|
| 193 |
}
|
|
@@ -199,6 +203,9 @@ export async function PUT(request: NextRequest) {
|
|
| 199 |
const auth = requireRole(request, 'operator');
|
| 200 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 201 |
|
|
|
|
|
|
|
|
|
|
| 202 |
try {
|
| 203 |
const db = getDatabase();
|
| 204 |
const { tasks } = await request.json();
|
|
@@ -254,7 +261,7 @@ export async function PUT(request: NextRequest) {
|
|
| 254 |
|
| 255 |
return NextResponse.json({ success: true, updated: tasks.length });
|
| 256 |
} catch (error) {
|
| 257 |
-
|
| 258 |
const message = error instanceof Error ? error.message : 'Failed to update tasks'
|
| 259 |
if (message.includes('Aegis approval required')) {
|
| 260 |
return NextResponse.json({ error: message }, { status: 403 });
|
|
|
|
| 2 |
import { getDatabase, Task, db_helpers } from '@/lib/db';
|
| 3 |
import { eventBus } from '@/lib/event-bus';
|
| 4 |
import { requireRole } from '@/lib/auth';
|
| 5 |
+
import { mutationLimiter } from '@/lib/rate-limit';
|
| 6 |
+
import { logger } from '@/lib/logger';
|
| 7 |
+
import { validateBody, createTaskSchema } from '@/lib/validation';
|
| 8 |
|
| 9 |
function hasAegisApproval(db: ReturnType<typeof getDatabase>, taskId: number): boolean {
|
| 10 |
const review = db.prepare(`
|
|
|
|
| 86 |
|
| 87 |
return NextResponse.json({ tasks: tasksWithParsedData, total: countRow.total, page: Math.floor(offset / limit) + 1, limit });
|
| 88 |
} catch (error) {
|
| 89 |
+
logger.error({ err: error }, 'GET /api/tasks error');
|
| 90 |
return NextResponse.json({ error: 'Failed to fetch tasks' }, { status: 500 });
|
| 91 |
}
|
| 92 |
}
|
|
|
|
| 98 |
const auth = requireRole(request, 'operator');
|
| 99 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 100 |
|
| 101 |
+
const rateCheck = mutationLimiter(request);
|
| 102 |
+
if (rateCheck) return rateCheck;
|
| 103 |
+
|
| 104 |
try {
|
| 105 |
const db = getDatabase();
|
| 106 |
+
const validated = await validateBody(request, createTaskSchema);
|
| 107 |
+
if ('error' in validated) return validated.error;
|
| 108 |
+
const body = validated.data;
|
| 109 |
|
| 110 |
const user = auth.user
|
| 111 |
const {
|
|
|
|
| 121 |
metadata = {}
|
| 122 |
} = body;
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
// Check for duplicate title
|
| 125 |
const existingTask = db.prepare('SELECT id FROM tasks WHERE title = ?').get(title);
|
| 126 |
if (existingTask) {
|
|
|
|
| 191 |
|
| 192 |
return NextResponse.json({ task: parsedTask }, { status: 201 });
|
| 193 |
} catch (error) {
|
| 194 |
+
logger.error({ err: error }, 'POST /api/tasks error');
|
| 195 |
return NextResponse.json({ error: 'Failed to create task' }, { status: 500 });
|
| 196 |
}
|
| 197 |
}
|
|
|
|
| 203 |
const auth = requireRole(request, 'operator');
|
| 204 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
| 205 |
|
| 206 |
+
const rateCheck = mutationLimiter(request);
|
| 207 |
+
if (rateCheck) return rateCheck;
|
| 208 |
+
|
| 209 |
try {
|
| 210 |
const db = getDatabase();
|
| 211 |
const { tasks } = await request.json();
|
|
|
|
| 261 |
|
| 262 |
return NextResponse.json({ success: true, updated: tasks.length });
|
| 263 |
} catch (error) {
|
| 264 |
+
logger.error({ err: error }, 'PUT /api/tasks error');
|
| 265 |
const message = error instanceof Error ? error.message : 'Failed to update tasks'
|
| 266 |
if (message.includes('Aegis approval required')) {
|
| 267 |
return NextResponse.json({ error: message }, { status: 403 });
|
src/app/api/webhooks/route.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { NextRequest, NextResponse } from 'next/server'
|
|
| 2 |
import { getDatabase } from '@/lib/db'
|
| 3 |
import { requireRole } from '@/lib/auth'
|
| 4 |
import { randomBytes, createHmac } from 'crypto'
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
/**
|
| 7 |
* GET /api/webhooks - List all webhooks with delivery stats
|
|
@@ -31,7 +34,7 @@ export async function GET(request: NextRequest) {
|
|
| 31 |
|
| 32 |
return NextResponse.json({ webhooks: result })
|
| 33 |
} catch (error) {
|
| 34 |
-
|
| 35 |
return NextResponse.json({ error: 'Failed to fetch webhooks' }, { status: 500 })
|
| 36 |
}
|
| 37 |
}
|
|
@@ -43,32 +46,26 @@ export async function POST(request: NextRequest) {
|
|
| 43 |
const auth = requireRole(request, 'admin')
|
| 44 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 45 |
|
|
|
|
|
|
|
|
|
|
| 46 |
try {
|
| 47 |
const db = getDatabase()
|
| 48 |
-
const
|
|
|
|
|
|
|
| 49 |
const { name, url, events, generate_secret } = body
|
| 50 |
|
| 51 |
-
if (!name || !url) {
|
| 52 |
-
return NextResponse.json({ error: 'Name and URL are required' }, { status: 400 })
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
// Validate URL
|
| 56 |
-
try {
|
| 57 |
-
new URL(url)
|
| 58 |
-
} catch {
|
| 59 |
-
return NextResponse.json({ error: 'Invalid URL' }, { status: 400 })
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
const secret = generate_secret !== false ? randomBytes(32).toString('hex') : null
|
| 63 |
const eventsJson = JSON.stringify(events || ['*'])
|
| 64 |
|
| 65 |
-
const
|
| 66 |
INSERT INTO webhooks (name, url, secret, events, created_by)
|
| 67 |
VALUES (?, ?, ?, ?, ?)
|
| 68 |
`).run(name, url, secret, eventsJson, auth.user.username)
|
| 69 |
|
| 70 |
return NextResponse.json({
|
| 71 |
-
id:
|
| 72 |
name,
|
| 73 |
url,
|
| 74 |
secret, // Show full secret only on creation
|
|
@@ -77,7 +74,7 @@ export async function POST(request: NextRequest) {
|
|
| 77 |
message: 'Webhook created. Save the secret - it won\'t be shown again in full.',
|
| 78 |
})
|
| 79 |
} catch (error) {
|
| 80 |
-
|
| 81 |
return NextResponse.json({ error: 'Failed to create webhook' }, { status: 500 })
|
| 82 |
}
|
| 83 |
}
|
|
@@ -89,6 +86,9 @@ export async function PUT(request: NextRequest) {
|
|
| 89 |
const auth = requireRole(request, 'admin')
|
| 90 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 91 |
|
|
|
|
|
|
|
|
|
|
| 92 |
try {
|
| 93 |
const db = getDatabase()
|
| 94 |
const body = await request.json()
|
|
@@ -132,7 +132,7 @@ export async function PUT(request: NextRequest) {
|
|
| 132 |
...(newSecret ? { secret: newSecret, message: 'New secret generated. Save it now.' } : {}),
|
| 133 |
})
|
| 134 |
} catch (error) {
|
| 135 |
-
|
| 136 |
return NextResponse.json({ error: 'Failed to update webhook' }, { status: 500 })
|
| 137 |
}
|
| 138 |
}
|
|
@@ -144,6 +144,9 @@ export async function DELETE(request: NextRequest) {
|
|
| 144 |
const auth = requireRole(request, 'admin')
|
| 145 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 146 |
|
|
|
|
|
|
|
|
|
|
| 147 |
try {
|
| 148 |
const db = getDatabase()
|
| 149 |
let body: any
|
|
@@ -164,7 +167,7 @@ export async function DELETE(request: NextRequest) {
|
|
| 164 |
|
| 165 |
return NextResponse.json({ success: true, deleted: result.changes })
|
| 166 |
} catch (error) {
|
| 167 |
-
|
| 168 |
return NextResponse.json({ error: 'Failed to delete webhook' }, { status: 500 })
|
| 169 |
}
|
| 170 |
}
|
|
|
|
| 2 |
import { getDatabase } from '@/lib/db'
|
| 3 |
import { requireRole } from '@/lib/auth'
|
| 4 |
import { randomBytes, createHmac } from 'crypto'
|
| 5 |
+
import { mutationLimiter } from '@/lib/rate-limit'
|
| 6 |
+
import { logger } from '@/lib/logger'
|
| 7 |
+
import { validateBody, createWebhookSchema } from '@/lib/validation'
|
| 8 |
|
| 9 |
/**
|
| 10 |
* GET /api/webhooks - List all webhooks with delivery stats
|
|
|
|
| 34 |
|
| 35 |
return NextResponse.json({ webhooks: result })
|
| 36 |
} catch (error) {
|
| 37 |
+
logger.error({ err: error }, 'GET /api/webhooks error')
|
| 38 |
return NextResponse.json({ error: 'Failed to fetch webhooks' }, { status: 500 })
|
| 39 |
}
|
| 40 |
}
|
|
|
|
| 46 |
const auth = requireRole(request, 'admin')
|
| 47 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 48 |
|
| 49 |
+
const rateCheck = mutationLimiter(request)
|
| 50 |
+
if (rateCheck) return rateCheck
|
| 51 |
+
|
| 52 |
try {
|
| 53 |
const db = getDatabase()
|
| 54 |
+
const validated = await validateBody(request, createWebhookSchema)
|
| 55 |
+
if ('error' in validated) return validated.error
|
| 56 |
+
const body = validated.data
|
| 57 |
const { name, url, events, generate_secret } = body
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
const secret = generate_secret !== false ? randomBytes(32).toString('hex') : null
|
| 60 |
const eventsJson = JSON.stringify(events || ['*'])
|
| 61 |
|
| 62 |
+
const dbResult = db.prepare(`
|
| 63 |
INSERT INTO webhooks (name, url, secret, events, created_by)
|
| 64 |
VALUES (?, ?, ?, ?, ?)
|
| 65 |
`).run(name, url, secret, eventsJson, auth.user.username)
|
| 66 |
|
| 67 |
return NextResponse.json({
|
| 68 |
+
id: dbResult.lastInsertRowid,
|
| 69 |
name,
|
| 70 |
url,
|
| 71 |
secret, // Show full secret only on creation
|
|
|
|
| 74 |
message: 'Webhook created. Save the secret - it won\'t be shown again in full.',
|
| 75 |
})
|
| 76 |
} catch (error) {
|
| 77 |
+
logger.error({ err: error }, 'POST /api/webhooks error')
|
| 78 |
return NextResponse.json({ error: 'Failed to create webhook' }, { status: 500 })
|
| 79 |
}
|
| 80 |
}
|
|
|
|
| 86 |
const auth = requireRole(request, 'admin')
|
| 87 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 88 |
|
| 89 |
+
const rateCheck = mutationLimiter(request)
|
| 90 |
+
if (rateCheck) return rateCheck
|
| 91 |
+
|
| 92 |
try {
|
| 93 |
const db = getDatabase()
|
| 94 |
const body = await request.json()
|
|
|
|
| 132 |
...(newSecret ? { secret: newSecret, message: 'New secret generated. Save it now.' } : {}),
|
| 133 |
})
|
| 134 |
} catch (error) {
|
| 135 |
+
logger.error({ err: error }, 'PUT /api/webhooks error')
|
| 136 |
return NextResponse.json({ error: 'Failed to update webhook' }, { status: 500 })
|
| 137 |
}
|
| 138 |
}
|
|
|
|
| 144 |
const auth = requireRole(request, 'admin')
|
| 145 |
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
| 146 |
|
| 147 |
+
const rateCheck = mutationLimiter(request)
|
| 148 |
+
if (rateCheck) return rateCheck
|
| 149 |
+
|
| 150 |
try {
|
| 151 |
const db = getDatabase()
|
| 152 |
let body: any
|
|
|
|
| 167 |
|
| 168 |
return NextResponse.json({ success: true, deleted: result.changes })
|
| 169 |
} catch (error) {
|
| 170 |
+
logger.error({ err: error }, 'DELETE /api/webhooks error')
|
| 171 |
return NextResponse.json({ error: 'Failed to delete webhook' }, { status: 500 })
|
| 172 |
}
|
| 173 |
}
|
src/app/login/page.tsx
CHANGED
|
@@ -110,7 +110,7 @@ export default function LoginPage() {
|
|
| 110 |
|
| 111 |
<form onSubmit={handleSubmit} className="space-y-4">
|
| 112 |
{error && (
|
| 113 |
-
<div className="p-3 rounded-lg bg-destructive/10 border border-destructive/20 text-sm text-destructive">
|
| 114 |
{error}
|
| 115 |
</div>
|
| 116 |
)}
|
|
@@ -127,6 +127,7 @@ export default function LoginPage() {
|
|
| 127 |
autoComplete="username"
|
| 128 |
autoFocus
|
| 129 |
required
|
|
|
|
| 130 |
/>
|
| 131 |
</div>
|
| 132 |
|
|
@@ -141,6 +142,7 @@ export default function LoginPage() {
|
|
| 141 |
placeholder="Enter password"
|
| 142 |
autoComplete="current-password"
|
| 143 |
required
|
|
|
|
| 144 |
/>
|
| 145 |
</div>
|
| 146 |
|
|
|
|
| 110 |
|
| 111 |
<form onSubmit={handleSubmit} className="space-y-4">
|
| 112 |
{error && (
|
| 113 |
+
<div role="alert" className="p-3 rounded-lg bg-destructive/10 border border-destructive/20 text-sm text-destructive">
|
| 114 |
{error}
|
| 115 |
</div>
|
| 116 |
)}
|
|
|
|
| 127 |
autoComplete="username"
|
| 128 |
autoFocus
|
| 129 |
required
|
| 130 |
+
aria-required="true"
|
| 131 |
/>
|
| 132 |
</div>
|
| 133 |
|
|
|
|
| 142 |
placeholder="Enter password"
|
| 143 |
autoComplete="current-password"
|
| 144 |
required
|
| 145 |
+
aria-required="true"
|
| 146 |
/>
|
| 147 |
</div>
|
| 148 |
|
src/app/page.tsx
CHANGED
|
@@ -29,6 +29,7 @@ import { AlertRulesPanel } from '@/components/panels/alert-rules-panel'
|
|
| 29 |
import { MultiGatewayPanel } from '@/components/panels/multi-gateway-panel'
|
| 30 |
import { SuperAdminPanel } from '@/components/panels/super-admin-panel'
|
| 31 |
import { ChatPanel } from '@/components/chat/chat-panel'
|
|
|
|
| 32 |
import { useWebSocket } from '@/lib/websocket'
|
| 33 |
import { useServerEvents } from '@/lib/use-server-events'
|
| 34 |
import { useMissionControl } from '@/store'
|
|
@@ -86,8 +87,12 @@ export default function Home() {
|
|
| 86 |
{/* Center: Header + Content */}
|
| 87 |
<div className="flex-1 flex flex-col min-w-0">
|
| 88 |
<HeaderBar />
|
| 89 |
-
<main className="flex-1 overflow-auto pb-16 md:pb-0">
|
| 90 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
</main>
|
| 92 |
</div>
|
| 93 |
|
|
|
|
| 29 |
import { MultiGatewayPanel } from '@/components/panels/multi-gateway-panel'
|
| 30 |
import { SuperAdminPanel } from '@/components/panels/super-admin-panel'
|
| 31 |
import { ChatPanel } from '@/components/chat/chat-panel'
|
| 32 |
+
import { ErrorBoundary } from '@/components/ErrorBoundary'
|
| 33 |
import { useWebSocket } from '@/lib/websocket'
|
| 34 |
import { useServerEvents } from '@/lib/use-server-events'
|
| 35 |
import { useMissionControl } from '@/store'
|
|
|
|
| 87 |
{/* Center: Header + Content */}
|
| 88 |
<div className="flex-1 flex flex-col min-w-0">
|
| 89 |
<HeaderBar />
|
| 90 |
+
<main className="flex-1 overflow-auto pb-16 md:pb-0" role="main">
|
| 91 |
+
<div aria-live="polite">
|
| 92 |
+
<ErrorBoundary key={activeTab}>
|
| 93 |
+
<ContentRouter tab={activeTab} />
|
| 94 |
+
</ErrorBoundary>
|
| 95 |
+
</div>
|
| 96 |
</main>
|
| 97 |
</div>
|
| 98 |
|
src/components/ErrorBoundary.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { Component, type ErrorInfo, type ReactNode } from 'react'
|
| 4 |
+
|
| 5 |
+
interface Props {
|
| 6 |
+
children: ReactNode
|
| 7 |
+
fallback?: ReactNode
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
interface State {
|
| 11 |
+
hasError: boolean
|
| 12 |
+
error: Error | null
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export class ErrorBoundary extends Component<Props, State> {
|
| 16 |
+
constructor(props: Props) {
|
| 17 |
+
super(props)
|
| 18 |
+
this.state = { hasError: false, error: null }
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
static getDerivedStateFromError(error: Error): State {
|
| 22 |
+
return { hasError: true, error }
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
| 26 |
+
console.error('Panel error:', error, errorInfo)
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
render() {
|
| 30 |
+
if (this.state.hasError) {
|
| 31 |
+
if (this.props.fallback) return this.props.fallback
|
| 32 |
+
|
| 33 |
+
return (
|
| 34 |
+
<div className="flex flex-col items-center justify-center h-full min-h-[200px] p-8 text-center">
|
| 35 |
+
<div className="w-12 h-12 rounded-full bg-destructive/10 flex items-center justify-center mb-4">
|
| 36 |
+
<svg className="w-6 h-6 text-destructive" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 37 |
+
<circle cx="12" cy="12" r="10" />
|
| 38 |
+
<line x1="12" y1="8" x2="12" y2="12" />
|
| 39 |
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
| 40 |
+
</svg>
|
| 41 |
+
</div>
|
| 42 |
+
<h3 className="text-lg font-semibold text-foreground mb-2">Something went wrong</h3>
|
| 43 |
+
<p className="text-sm text-muted-foreground mb-4 max-w-md">
|
| 44 |
+
{this.state.error?.message || 'An unexpected error occurred in this panel.'}
|
| 45 |
+
</p>
|
| 46 |
+
<button
|
| 47 |
+
onClick={() => this.setState({ hasError: false, error: null })}
|
| 48 |
+
className="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
|
| 49 |
+
>
|
| 50 |
+
Try again
|
| 51 |
+
</button>
|
| 52 |
+
</div>
|
| 53 |
+
)
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
return this.props.children
|
| 57 |
+
}
|
| 58 |
+
}
|
src/components/layout/header-bar.tsx
CHANGED
|
@@ -127,7 +127,7 @@ export function HeaderBar() {
|
|
| 127 |
}
|
| 128 |
|
| 129 |
return (
|
| 130 |
-
<header className="h-12 bg-card/80 backdrop-blur-sm border-b border-border px-4 flex items-center justify-between shrink-0">
|
| 131 |
{/* Left: Page title + breadcrumb */}
|
| 132 |
<div className="flex items-center gap-3">
|
| 133 |
<h1 className="text-sm font-semibold text-foreground">
|
|
|
|
| 127 |
}
|
| 128 |
|
| 129 |
return (
|
| 130 |
+
<header role="banner" aria-label="Application header" className="h-12 bg-card/80 backdrop-blur-sm border-b border-border px-4 flex items-center justify-between shrink-0">
|
| 131 |
{/* Left: Page title + breadcrumb */}
|
| 132 |
<div className="flex items-center gap-3">
|
| 133 |
<h1 className="text-sm font-semibold text-foreground">
|
src/components/layout/nav-rail.tsx
CHANGED
|
@@ -84,6 +84,8 @@ export function NavRail() {
|
|
| 84 |
<>
|
| 85 |
{/* Desktop: Grouped sidebar */}
|
| 86 |
<nav
|
|
|
|
|
|
|
| 87 |
className={`hidden md:flex flex-col bg-card border-r border-border shrink-0 transition-all duration-200 ease-in-out ${
|
| 88 |
sidebarExpanded ? 'w-[220px]' : 'w-14'
|
| 89 |
}`}
|
|
@@ -199,6 +201,7 @@ function NavButton({ item, active, expanded, onClick }: {
|
|
| 199 |
return (
|
| 200 |
<button
|
| 201 |
onClick={onClick}
|
|
|
|
| 202 |
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-lg text-left transition-smooth relative ${
|
| 203 |
active
|
| 204 |
? 'bg-primary/15 text-primary'
|
|
@@ -218,6 +221,7 @@ function NavButton({ item, active, expanded, onClick }: {
|
|
| 218 |
<button
|
| 219 |
onClick={onClick}
|
| 220 |
title={item.label}
|
|
|
|
| 221 |
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-smooth group relative ${
|
| 222 |
active
|
| 223 |
? 'bg-primary/15 text-primary'
|
|
|
|
| 84 |
<>
|
| 85 |
{/* Desktop: Grouped sidebar */}
|
| 86 |
<nav
|
| 87 |
+
role="navigation"
|
| 88 |
+
aria-label="Main navigation"
|
| 89 |
className={`hidden md:flex flex-col bg-card border-r border-border shrink-0 transition-all duration-200 ease-in-out ${
|
| 90 |
sidebarExpanded ? 'w-[220px]' : 'w-14'
|
| 91 |
}`}
|
|
|
|
| 201 |
return (
|
| 202 |
<button
|
| 203 |
onClick={onClick}
|
| 204 |
+
aria-current={active ? 'page' : undefined}
|
| 205 |
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-lg text-left transition-smooth relative ${
|
| 206 |
active
|
| 207 |
? 'bg-primary/15 text-primary'
|
|
|
|
| 221 |
<button
|
| 222 |
onClick={onClick}
|
| 223 |
title={item.label}
|
| 224 |
+
aria-current={active ? 'page' : undefined}
|
| 225 |
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-smooth group relative ${
|
| 226 |
active
|
| 227 |
? 'bg-primary/15 text-primary'
|
src/components/panels/session-details-panel.tsx
CHANGED
|
@@ -26,6 +26,7 @@ export function SessionDetailsPanel() {
|
|
| 26 |
|
| 27 |
useSmartPoll(loadSessions, 60000, { pauseWhenConnected: true })
|
| 28 |
|
|
|
|
| 29 |
const [sessionFilter, setSessionFilter] = useState<'all' | 'active' | 'idle'>('all')
|
| 30 |
const [sortBy, setSortBy] = useState<'age' | 'tokens' | 'model'>('age')
|
| 31 |
const [expandedSession, setExpandedSession] = useState<string | null>(null)
|
|
@@ -323,36 +324,80 @@ export function SessionDetailsPanel() {
|
|
| 323 |
{/* Actions */}
|
| 324 |
<div className="flex space-x-2">
|
| 325 |
<button
|
| 326 |
-
className="px-3 py-1 text-xs bg-blue-500/20 text-blue-400 border border-blue-500/30 rounded hover:bg-blue-500/30 transition-colors"
|
| 327 |
-
|
|
|
|
| 328 |
e.stopPropagation()
|
| 329 |
-
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
}}
|
| 332 |
>
|
| 333 |
-
Monitor
|
| 334 |
</button>
|
| 335 |
<button
|
| 336 |
-
className="px-3 py-1 text-xs bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 rounded hover:bg-yellow-500/30 transition-colors"
|
| 337 |
-
|
|
|
|
| 338 |
e.stopPropagation()
|
| 339 |
-
|
| 340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
}}
|
| 342 |
>
|
| 343 |
-
Pause
|
| 344 |
</button>
|
| 345 |
<button
|
| 346 |
-
className="px-3 py-1 text-xs bg-red-500/20 text-red-400 border border-red-500/30 rounded hover:bg-red-500/30 transition-colors"
|
| 347 |
-
|
|
|
|
| 348 |
e.stopPropagation()
|
| 349 |
-
if (confirm('Are you sure you want to terminate this session?'))
|
| 350 |
-
|
| 351 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
}
|
| 353 |
}}
|
| 354 |
>
|
| 355 |
-
Terminate
|
| 356 |
</button>
|
| 357 |
</div>
|
| 358 |
</div>
|
|
|
|
| 26 |
|
| 27 |
useSmartPoll(loadSessions, 60000, { pauseWhenConnected: true })
|
| 28 |
|
| 29 |
+
const [controllingSession, setControllingSession] = useState<string | null>(null)
|
| 30 |
const [sessionFilter, setSessionFilter] = useState<'all' | 'active' | 'idle'>('all')
|
| 31 |
const [sortBy, setSortBy] = useState<'age' | 'tokens' | 'model'>('age')
|
| 32 |
const [expandedSession, setExpandedSession] = useState<string | null>(null)
|
|
|
|
| 324 |
{/* Actions */}
|
| 325 |
<div className="flex space-x-2">
|
| 326 |
<button
|
| 327 |
+
className="px-3 py-1 text-xs bg-blue-500/20 text-blue-400 border border-blue-500/30 rounded hover:bg-blue-500/30 transition-colors disabled:opacity-50"
|
| 328 |
+
disabled={controllingSession !== null}
|
| 329 |
+
onClick={async (e) => {
|
| 330 |
e.stopPropagation()
|
| 331 |
+
setControllingSession(`monitor-${session.id}`)
|
| 332 |
+
try {
|
| 333 |
+
const res = await fetch(`/api/sessions/${session.id}/control`, {
|
| 334 |
+
method: 'POST',
|
| 335 |
+
headers: { 'Content-Type': 'application/json' },
|
| 336 |
+
body: JSON.stringify({ action: 'monitor' }),
|
| 337 |
+
})
|
| 338 |
+
if (!res.ok) {
|
| 339 |
+
const data = await res.json()
|
| 340 |
+
alert(data.error || 'Failed to monitor session')
|
| 341 |
+
}
|
| 342 |
+
} catch {
|
| 343 |
+
alert('Failed to monitor session')
|
| 344 |
+
} finally {
|
| 345 |
+
setControllingSession(null)
|
| 346 |
+
}
|
| 347 |
}}
|
| 348 |
>
|
| 349 |
+
{controllingSession === `monitor-${session.id}` ? 'Working...' : 'Monitor'}
|
| 350 |
</button>
|
| 351 |
<button
|
| 352 |
+
className="px-3 py-1 text-xs bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 rounded hover:bg-yellow-500/30 transition-colors disabled:opacity-50"
|
| 353 |
+
disabled={controllingSession !== null}
|
| 354 |
+
onClick={async (e) => {
|
| 355 |
e.stopPropagation()
|
| 356 |
+
setControllingSession(`pause-${session.id}`)
|
| 357 |
+
try {
|
| 358 |
+
const res = await fetch(`/api/sessions/${session.id}/control`, {
|
| 359 |
+
method: 'POST',
|
| 360 |
+
headers: { 'Content-Type': 'application/json' },
|
| 361 |
+
body: JSON.stringify({ action: 'pause' }),
|
| 362 |
+
})
|
| 363 |
+
if (!res.ok) {
|
| 364 |
+
const data = await res.json()
|
| 365 |
+
alert(data.error || 'Failed to pause session')
|
| 366 |
+
}
|
| 367 |
+
} catch {
|
| 368 |
+
alert('Failed to pause session')
|
| 369 |
+
} finally {
|
| 370 |
+
setControllingSession(null)
|
| 371 |
+
}
|
| 372 |
}}
|
| 373 |
>
|
| 374 |
+
{controllingSession === `pause-${session.id}` ? 'Working...' : 'Pause'}
|
| 375 |
</button>
|
| 376 |
<button
|
| 377 |
+
className="px-3 py-1 text-xs bg-red-500/20 text-red-400 border border-red-500/30 rounded hover:bg-red-500/30 transition-colors disabled:opacity-50"
|
| 378 |
+
disabled={controllingSession !== null}
|
| 379 |
+
onClick={async (e) => {
|
| 380 |
e.stopPropagation()
|
| 381 |
+
if (!window.confirm('Are you sure you want to terminate this session?')) return
|
| 382 |
+
setControllingSession(`terminate-${session.id}`)
|
| 383 |
+
try {
|
| 384 |
+
const res = await fetch(`/api/sessions/${session.id}/control`, {
|
| 385 |
+
method: 'POST',
|
| 386 |
+
headers: { 'Content-Type': 'application/json' },
|
| 387 |
+
body: JSON.stringify({ action: 'terminate' }),
|
| 388 |
+
})
|
| 389 |
+
if (!res.ok) {
|
| 390 |
+
const data = await res.json()
|
| 391 |
+
alert(data.error || 'Failed to terminate session')
|
| 392 |
+
}
|
| 393 |
+
} catch {
|
| 394 |
+
alert('Failed to terminate session')
|
| 395 |
+
} finally {
|
| 396 |
+
setControllingSession(null)
|
| 397 |
}
|
| 398 |
}}
|
| 399 |
>
|
| 400 |
+
{controllingSession === `terminate-${session.id}` ? 'Working...' : 'Terminate'}
|
| 401 |
</button>
|
| 402 |
</div>
|
| 403 |
</div>
|
src/lib/db.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { config, ensureDirExists } from './config';
|
|
| 4 |
import { runMigrations } from './migrations';
|
| 5 |
import { eventBus } from './event-bus';
|
| 6 |
import { hashPassword } from './password';
|
|
|
|
| 7 |
|
| 8 |
// Database file location
|
| 9 |
const DB_PATH = config.dbPath;
|
|
@@ -63,9 +64,9 @@ function initializeSchema() {
|
|
| 63 |
}
|
| 64 |
}
|
| 65 |
|
| 66 |
-
|
| 67 |
} catch (error) {
|
| 68 |
-
|
| 69 |
throw error;
|
| 70 |
}
|
| 71 |
}
|
|
@@ -83,7 +84,7 @@ function seedAdminUserFromEnv(dbConn: Database.Database): void {
|
|
| 83 |
VALUES (?, ?, ?, ?)
|
| 84 |
`).run(username, displayName, hashPassword(password), 'admin')
|
| 85 |
|
| 86 |
-
|
| 87 |
}
|
| 88 |
|
| 89 |
/**
|
|
@@ -460,7 +461,7 @@ if (typeof window === 'undefined') { // Only run on server side
|
|
| 460 |
try {
|
| 461 |
getDatabase();
|
| 462 |
} catch (error) {
|
| 463 |
-
|
| 464 |
}
|
| 465 |
}
|
| 466 |
|
|
|
|
| 4 |
import { runMigrations } from './migrations';
|
| 5 |
import { eventBus } from './event-bus';
|
| 6 |
import { hashPassword } from './password';
|
| 7 |
+
import { logger } from './logger';
|
| 8 |
|
| 9 |
// Database file location
|
| 10 |
const DB_PATH = config.dbPath;
|
|
|
|
| 64 |
}
|
| 65 |
}
|
| 66 |
|
| 67 |
+
logger.info('Database migrations applied successfully');
|
| 68 |
} catch (error) {
|
| 69 |
+
logger.error({ err: error }, 'Failed to apply database migrations');
|
| 70 |
throw error;
|
| 71 |
}
|
| 72 |
}
|
|
|
|
| 84 |
VALUES (?, ?, ?, ?)
|
| 85 |
`).run(username, displayName, hashPassword(password), 'admin')
|
| 86 |
|
| 87 |
+
logger.info(`Seeded admin user: ${username}`)
|
| 88 |
}
|
| 89 |
|
| 90 |
/**
|
|
|
|
| 461 |
try {
|
| 462 |
getDatabase();
|
| 463 |
} catch (error) {
|
| 464 |
+
logger.error({ err: error }, 'Failed to initialize database');
|
| 465 |
}
|
| 466 |
}
|
| 467 |
|
src/lib/logger.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pino from 'pino'
|
| 2 |
+
|
| 3 |
+
export const logger = pino({
|
| 4 |
+
level: process.env.LOG_LEVEL || 'info',
|
| 5 |
+
...(process.env.NODE_ENV !== 'production' && {
|
| 6 |
+
transport: {
|
| 7 |
+
target: 'pino-pretty',
|
| 8 |
+
options: { colorize: true },
|
| 9 |
+
},
|
| 10 |
+
}),
|
| 11 |
+
})
|
src/lib/models.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface ModelConfig {
|
| 2 |
+
alias: string
|
| 3 |
+
name: string
|
| 4 |
+
provider: string
|
| 5 |
+
description: string
|
| 6 |
+
costPer1k: number
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export const MODEL_CATALOG: ModelConfig[] = [
|
| 10 |
+
{ alias: 'haiku', name: 'anthropic/claude-3-5-haiku-latest', provider: 'anthropic', description: 'Ultra-cheap, simple tasks', costPer1k: 0.25 },
|
| 11 |
+
{ alias: 'sonnet', name: 'anthropic/claude-sonnet-4-20250514', provider: 'anthropic', description: 'Standard workhorse', costPer1k: 3.0 },
|
| 12 |
+
{ alias: 'opus', name: 'anthropic/claude-opus-4-5', provider: 'anthropic', description: 'Premium quality', costPer1k: 15.0 },
|
| 13 |
+
{ alias: 'deepseek', name: 'ollama/deepseek-r1:14b', provider: 'ollama', description: 'Local reasoning (free)', costPer1k: 0.0 },
|
| 14 |
+
{ alias: 'groq-fast', name: 'groq/llama-3.1-8b-instant', provider: 'groq', description: '840 tok/s, ultra fast', costPer1k: 0.05 },
|
| 15 |
+
{ alias: 'groq', name: 'groq/llama-3.3-70b-versatile', provider: 'groq', description: 'Fast + quality balance', costPer1k: 0.59 },
|
| 16 |
+
{ alias: 'kimi', name: 'moonshot/kimi-k2.5', provider: 'moonshot', description: 'Alternative provider', costPer1k: 1.0 },
|
| 17 |
+
{ alias: 'minimax', name: 'minimax/minimax-m2.1', provider: 'minimax', description: 'Cost-effective (1/10th price), strong coding', costPer1k: 0.3 },
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
export function getModelByAlias(alias: string): ModelConfig | undefined {
|
| 21 |
+
return MODEL_CATALOG.find(m => m.alias === alias)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export function getModelByName(name: string): ModelConfig | undefined {
|
| 25 |
+
return MODEL_CATALOG.find(m => m.name === name)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export function getAllModels(): ModelConfig[] {
|
| 29 |
+
return [...MODEL_CATALOG]
|
| 30 |
+
}
|
src/lib/rate-limit.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server'
|
| 2 |
+
|
| 3 |
+
interface RateLimitEntry {
|
| 4 |
+
count: number
|
| 5 |
+
resetAt: number
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
interface RateLimiterOptions {
|
| 9 |
+
windowMs: number
|
| 10 |
+
maxRequests: number
|
| 11 |
+
message?: string
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export function createRateLimiter(options: RateLimiterOptions) {
|
| 15 |
+
const store = new Map<string, RateLimitEntry>()
|
| 16 |
+
|
| 17 |
+
// Periodic cleanup every 60s
|
| 18 |
+
const cleanupInterval = setInterval(() => {
|
| 19 |
+
const now = Date.now()
|
| 20 |
+
for (const [key, entry] of store) {
|
| 21 |
+
if (now > entry.resetAt) store.delete(key)
|
| 22 |
+
}
|
| 23 |
+
}, 60_000)
|
| 24 |
+
// Don't prevent process exit
|
| 25 |
+
if (cleanupInterval.unref) cleanupInterval.unref()
|
| 26 |
+
|
| 27 |
+
return function checkRateLimit(request: Request): NextResponse | null {
|
| 28 |
+
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'
|
| 29 |
+
const now = Date.now()
|
| 30 |
+
const entry = store.get(ip)
|
| 31 |
+
|
| 32 |
+
if (!entry || now > entry.resetAt) {
|
| 33 |
+
store.set(ip, { count: 1, resetAt: now + options.windowMs })
|
| 34 |
+
return null
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
entry.count++
|
| 38 |
+
if (entry.count > options.maxRequests) {
|
| 39 |
+
return NextResponse.json(
|
| 40 |
+
{ error: options.message || 'Too many requests. Please try again later.' },
|
| 41 |
+
{ status: 429 }
|
| 42 |
+
)
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
return null
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
export const loginLimiter = createRateLimiter({
|
| 50 |
+
windowMs: 60_000,
|
| 51 |
+
maxRequests: 5,
|
| 52 |
+
message: 'Too many login attempts. Try again in a minute.',
|
| 53 |
+
})
|
| 54 |
+
|
| 55 |
+
export const mutationLimiter = createRateLimiter({
|
| 56 |
+
windowMs: 60_000,
|
| 57 |
+
maxRequests: 60,
|
| 58 |
+
})
|
| 59 |
+
|
| 60 |
+
export const heavyLimiter = createRateLimiter({
|
| 61 |
+
windowMs: 60_000,
|
| 62 |
+
maxRequests: 10,
|
| 63 |
+
message: 'Too many requests for this resource. Please try again later.',
|
| 64 |
+
})
|
src/lib/scheduler.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { syncAgentsFromConfig } from './agent-sync'
|
|
| 3 |
import { config, ensureDirExists } from './config'
|
| 4 |
import { join, dirname } from 'path'
|
| 5 |
import { readdirSync, statSync, unlinkSync } from 'fs'
|
|
|
|
| 6 |
|
| 7 |
const BACKUP_DIR = join(dirname(config.dbPath), 'backups')
|
| 8 |
|
|
@@ -209,7 +210,7 @@ export function initScheduler() {
|
|
| 209 |
|
| 210 |
// Auto-sync agents from openclaw.json on startup
|
| 211 |
syncAgentsFromConfig('startup').catch(err => {
|
| 212 |
-
|
| 213 |
})
|
| 214 |
|
| 215 |
// Register tasks
|
|
@@ -247,7 +248,7 @@ export function initScheduler() {
|
|
| 247 |
|
| 248 |
// Start the tick loop
|
| 249 |
tickInterval = setInterval(tick, TICK_MS)
|
| 250 |
-
|
| 251 |
}
|
| 252 |
|
| 253 |
/** Calculate ms until next occurrence of a given hour (UTC) */
|
|
|
|
| 3 |
import { config, ensureDirExists } from './config'
|
| 4 |
import { join, dirname } from 'path'
|
| 5 |
import { readdirSync, statSync, unlinkSync } from 'fs'
|
| 6 |
+
import { logger } from './logger'
|
| 7 |
|
| 8 |
const BACKUP_DIR = join(dirname(config.dbPath), 'backups')
|
| 9 |
|
|
|
|
| 210 |
|
| 211 |
// Auto-sync agents from openclaw.json on startup
|
| 212 |
syncAgentsFromConfig('startup').catch(err => {
|
| 213 |
+
logger.warn({ err }, 'Agent auto-sync failed')
|
| 214 |
})
|
| 215 |
|
| 216 |
// Register tasks
|
|
|
|
| 248 |
|
| 249 |
// Start the tick loop
|
| 250 |
tickInterval = setInterval(tick, TICK_MS)
|
| 251 |
+
logger.info('Scheduler initialized - backup at ~3AM, cleanup at ~4AM, heartbeat every 5m')
|
| 252 |
}
|
| 253 |
|
| 254 |
/** Calculate ms until next occurrence of a given hour (UTC) */
|
src/lib/validation.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server'
|
| 2 |
+
import { ZodSchema, ZodError } from 'zod'
|
| 3 |
+
import { z } from 'zod'
|
| 4 |
+
|
| 5 |
+
export async function validateBody<T>(
|
| 6 |
+
request: Request,
|
| 7 |
+
schema: ZodSchema<T>
|
| 8 |
+
): Promise<{ data: T } | { error: NextResponse }> {
|
| 9 |
+
try {
|
| 10 |
+
const body = await request.json()
|
| 11 |
+
const data = schema.parse(body)
|
| 12 |
+
return { data }
|
| 13 |
+
} catch (err) {
|
| 14 |
+
if (err instanceof ZodError) {
|
| 15 |
+
const messages = err.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`)
|
| 16 |
+
return {
|
| 17 |
+
error: NextResponse.json(
|
| 18 |
+
{ error: 'Validation failed', details: messages },
|
| 19 |
+
{ status: 400 }
|
| 20 |
+
),
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
return {
|
| 24 |
+
error: NextResponse.json({ error: 'Invalid request body' }, { status: 400 }),
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export const createTaskSchema = z.object({
|
| 30 |
+
title: z.string().min(1, 'Title is required').max(500),
|
| 31 |
+
description: z.string().max(5000).optional(),
|
| 32 |
+
status: z.enum(['inbox', 'assigned', 'in_progress', 'review', 'done', 'blocked']).default('inbox'),
|
| 33 |
+
priority: z.enum(['critical', 'high', 'medium', 'low']).default('medium'),
|
| 34 |
+
assigned_to: z.string().max(100).optional(),
|
| 35 |
+
created_by: z.string().max(100).optional(),
|
| 36 |
+
due_date: z.number().optional(),
|
| 37 |
+
estimated_hours: z.number().min(0).optional(),
|
| 38 |
+
actual_hours: z.number().min(0).optional(),
|
| 39 |
+
tags: z.array(z.string()).default([] as string[]),
|
| 40 |
+
metadata: z.record(z.string(), z.unknown()).default({} as Record<string, unknown>),
|
| 41 |
+
})
|
| 42 |
+
|
| 43 |
+
export const updateTaskSchema = createTaskSchema.partial()
|
| 44 |
+
|
| 45 |
+
export const createAgentSchema = z.object({
|
| 46 |
+
name: z.string().min(1, 'Name is required').max(100),
|
| 47 |
+
role: z.string().min(1, 'Role is required').max(100).optional(),
|
| 48 |
+
session_key: z.string().max(200).optional(),
|
| 49 |
+
soul_content: z.string().max(50000).optional(),
|
| 50 |
+
status: z.enum(['online', 'offline', 'busy', 'idle', 'error']).default('offline'),
|
| 51 |
+
config: z.record(z.string(), z.unknown()).default({} as Record<string, unknown>),
|
| 52 |
+
template: z.string().max(100).optional(),
|
| 53 |
+
gateway_config: z.record(z.string(), z.unknown()).optional(),
|
| 54 |
+
write_to_gateway: z.boolean().optional(),
|
| 55 |
+
})
|
| 56 |
+
|
| 57 |
+
export const createWebhookSchema = z.object({
|
| 58 |
+
name: z.string().min(1, 'Name is required').max(200),
|
| 59 |
+
url: z.string().url('Invalid URL'),
|
| 60 |
+
events: z.array(z.string()).optional(),
|
| 61 |
+
generate_secret: z.boolean().optional(),
|
| 62 |
+
})
|
| 63 |
+
|
| 64 |
+
export const createAlertSchema = z.object({
|
| 65 |
+
name: z.string().min(1, 'Name is required').max(200),
|
| 66 |
+
description: z.string().max(1000).optional(),
|
| 67 |
+
entity_type: z.enum(['agent', 'task', 'session', 'activity']),
|
| 68 |
+
condition_field: z.string().min(1).max(100),
|
| 69 |
+
condition_operator: z.enum(['equals', 'not_equals', 'greater_than', 'less_than', 'contains', 'count_above', 'count_below', 'age_minutes_above']),
|
| 70 |
+
condition_value: z.string().min(1).max(500),
|
| 71 |
+
action_type: z.string().max(100).optional(),
|
| 72 |
+
action_config: z.record(z.string(), z.unknown()).optional(),
|
| 73 |
+
cooldown_minutes: z.number().min(1).max(10080).optional(),
|
| 74 |
+
})
|
src/lib/webhooks.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import { createHmac } from 'crypto'
|
| 2 |
import { eventBus, type ServerEvent } from './event-bus'
|
|
|
|
| 3 |
|
| 4 |
interface Webhook {
|
| 5 |
id: number
|
|
@@ -46,12 +47,12 @@ export function initWebhookListener() {
|
|
| 46 |
const isAgentError = event.type === 'agent.status_changed' && event.data?.status === 'error'
|
| 47 |
|
| 48 |
fireWebhooksAsync(webhookEventType, event.data).catch((err) => {
|
| 49 |
-
|
| 50 |
})
|
| 51 |
|
| 52 |
if (isAgentError) {
|
| 53 |
fireWebhooksAsync('agent.error', event.data).catch((err) => {
|
| 54 |
-
|
| 55 |
})
|
| 56 |
}
|
| 57 |
})
|
|
@@ -62,7 +63,7 @@ export function initWebhookListener() {
|
|
| 62 |
*/
|
| 63 |
export function fireWebhooks(eventType: string, payload: Record<string, any>) {
|
| 64 |
fireWebhooksAsync(eventType, payload).catch((err) => {
|
| 65 |
-
|
| 66 |
})
|
| 67 |
}
|
| 68 |
|
|
|
|
| 1 |
import { createHmac } from 'crypto'
|
| 2 |
import { eventBus, type ServerEvent } from './event-bus'
|
| 3 |
+
import { logger } from './logger'
|
| 4 |
|
| 5 |
interface Webhook {
|
| 6 |
id: number
|
|
|
|
| 47 |
const isAgentError = event.type === 'agent.status_changed' && event.data?.status === 'error'
|
| 48 |
|
| 49 |
fireWebhooksAsync(webhookEventType, event.data).catch((err) => {
|
| 50 |
+
logger.error({ err }, 'Webhook dispatch error')
|
| 51 |
})
|
| 52 |
|
| 53 |
if (isAgentError) {
|
| 54 |
fireWebhooksAsync('agent.error', event.data).catch((err) => {
|
| 55 |
+
logger.error({ err }, 'Webhook dispatch error')
|
| 56 |
})
|
| 57 |
}
|
| 58 |
})
|
|
|
|
| 63 |
*/
|
| 64 |
export function fireWebhooks(eventType: string, payload: Record<string, any>) {
|
| 65 |
fireWebhooksAsync(eventType, payload).catch((err) => {
|
| 66 |
+
logger.error({ err }, 'Webhook dispatch error')
|
| 67 |
})
|
| 68 |
}
|
| 69 |
|
src/store/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
import { create } from 'zustand'
|
| 4 |
import { subscribeWithSelector } from 'zustand/middleware'
|
|
|
|
| 5 |
|
| 6 |
// Enhanced types for Mission Control
|
| 7 |
export interface Session {
|
|
@@ -523,16 +524,7 @@ export const useMissionControl = create<MissionControlStore>()(
|
|
| 523 |
},
|
| 524 |
|
| 525 |
// Model Configuration
|
| 526 |
-
availableModels: [
|
| 527 |
-
{ alias: 'haiku', name: 'anthropic/claude-3-5-haiku-latest', provider: 'anthropic', description: 'Ultra-cheap, simple tasks', costPer1k: 0.25 },
|
| 528 |
-
{ alias: 'sonnet', name: 'anthropic/claude-sonnet-4-20250514', provider: 'anthropic', description: 'Standard workhorse', costPer1k: 3.0 },
|
| 529 |
-
{ alias: 'opus', name: 'anthropic/claude-opus-4-5', provider: 'anthropic', description: 'Premium quality', costPer1k: 15.0 },
|
| 530 |
-
{ alias: 'deepseek', name: 'ollama/deepseek-r1:14b', provider: 'ollama', description: 'Local reasoning (free)', costPer1k: 0.0 },
|
| 531 |
-
{ alias: 'groq-fast', name: 'groq/llama-3.1-8b-instant', provider: 'groq', description: '840 tok/s, ultra fast', costPer1k: 0.05 },
|
| 532 |
-
{ alias: 'groq', name: 'groq/llama-3.3-70b-versatile', provider: 'groq', description: 'Fast + quality balance', costPer1k: 0.59 },
|
| 533 |
-
{ alias: 'kimi', name: 'moonshot/kimi-k2.5', provider: 'moonshot', description: 'Alternative provider', costPer1k: 1.0 },
|
| 534 |
-
{ alias: 'minimax', name: 'minimax/minimax-m2.1', provider: 'minimax', description: 'Cost-effective (1/10th price), strong coding', costPer1k: 0.3 },
|
| 535 |
-
],
|
| 536 |
setAvailableModels: (models) => set({ availableModels: models }),
|
| 537 |
|
| 538 |
// Auth
|
|
|
|
| 2 |
|
| 3 |
import { create } from 'zustand'
|
| 4 |
import { subscribeWithSelector } from 'zustand/middleware'
|
| 5 |
+
import { MODEL_CATALOG } from '@/lib/models'
|
| 6 |
|
| 7 |
// Enhanced types for Mission Control
|
| 8 |
export interface Session {
|
|
|
|
| 524 |
},
|
| 525 |
|
| 526 |
// Model Configuration
|
| 527 |
+
availableModels: [...MODEL_CATALOG],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
setAvailableModels: (models) => set({ availableModels: models }),
|
| 529 |
|
| 530 |
// Auth
|