diff --git a/dashboard/backend/Dockerfile b/dashboard/backend/Dockerfile index 4b47235..c942bb8 100644 --- a/dashboard/backend/Dockerfile +++ b/dashboard/backend/Dockerfile @@ -30,13 +30,22 @@ ENV BYTELYST_COMMIT_SHA=${BYTELYST_COMMIT_SHA} \ RUN npm run build # --- Stage 2: Run --- -FROM node:20-alpine AS runner +# Use Debian slim (not Alpine) because vm-health-check.sh uses GNU df flags +# (--output=pcent, --output=avail) that BusyBox df does not support. +FROM node:20-slim AS runner WORKDIR /app/backend COPY backend/package.json backend/package-lock.json ./ RUN npm ci --omit=dev --ignore-scripts -RUN apk add --no-cache curl + +# Install tools needed by the VM management module: +# bash — vm-health-check.sh and vm-cleanup.sh require bash +# docker.io — docker CLI to communicate with the host daemon via socket +# python3 — used in inline python3 -c snippets inside the scripts +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl bash docker.io python3 \ + && rm -rf /var/lib/apt/lists/* COPY --from=builder /app/backend/dist ./dist diff --git a/dashboard/backend/package-lock.json b/dashboard/backend/package-lock.json index cb01ff1..75d45b6 100644 --- a/dashboard/backend/package-lock.json +++ b/dashboard/backend/package-lock.json @@ -8,9 +8,13 @@ "name": "@bytelyst/devops-backend", "version": "0.1.0", "dependencies": { + "@azure/cosmos": "^4.1.0", + "@azure/identity": "^4.5.0", + "@azure/keyvault-secrets": "^4.9.0", "@fastify/rate-limit": "^10.2.1", "@fastify/swagger": "^9.0.0", - "@fastify/swagger-ui": "^2.0.1", + "@fastify/swagger-ui": "^5.2.1", + "dotenv": "^16.4.5", "fastify": "^5.2.1", "fastify-sse-v2": "^4.2.2", "jose": "^6.1.2", @@ -18,11 +22,389 @@ }, "devDependencies": { "@types/node": "^25.0.3", + "@vitest/coverage-v8": "3.2.4", "tsx": "^4.21.0", "typescript": "^5.9.3", "vitest": "^3.1.2" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@azure-rest/core-client": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.6.0.tgz", + "integrity": "sha512-iuFKDm8XPzNxPfRjhyU5/xKZmcRDzSuEghXDHHk4MjBV/wFL34GmYVBZnn9wmuoLBeS1qAw9ceMdaeJBPcB1QQ==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.4.0.tgz", + "integrity": "sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/cosmos": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.9.3.tgz", + "integrity": "sha512-AWRj+yhw1lybutNcsHJ8syxWXnTLvc3CPwwdCwG1I0I71f25ZcBkxneTeoaB3X57+xl1nO+zJKUqfm0RhpGUFA==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-keys": "^4.9.0", + "@azure/logger": "^1.1.4", + "fast-json-stable-stringify": "^2.1.0", + "priorityqueuejs": "^2.0.0", + "semaphore": "^1.1.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/keyvault-common": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.1.0.tgz", + "integrity": "sha512-aCDidWuKY06LWQ4x7/8TIXK6iRqTaRWRL3t7T+LC+j1b07HtoIsOxP/tU90G4jCSBn5TAyUTCtA4MS/y5Hudaw==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz", + "integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/keyvault-secrets": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@azure/keyvault-secrets/-/keyvault-secrets-4.11.2.tgz", + "integrity": "sha512-ECj/kwZbZlQXj2kfWivSICbKwj6W3chmFhv8qUdauqYnjvZ0hWZBFSsZWux7W2nX3MP49PLUCusXk+hAg3pipg==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.1.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.11.0.tgz", + "integrity": "sha512-zkGNYS3TwY8lUpPIafAmsFCYZbgFixY9y/LZB9GUg0IILoHTqpN26j5OrkL1AQThh/YdZsawe4iWXfp85lFVxg==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.6.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.6.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.2.tgz", + "integrity": "sha512-hQjjsekAjB00cM1EmatWJlzhEoK2Qhz7Rj5gvM6tYf8iL7RM3tkxlpU9fG0+ofkulzg9AEEA6dIEnSmDr5ZqUA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.2.tgz", + "integrity": "sha512-toS+2AePxqyzb0YOKttDOOiSl3jrkK9aiqIvpurpis0O34QcIS5gToqrgT39p04Dpxw3YoUU0lxJKTpSFFfA6Q==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.6.2", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -466,13 +848,20 @@ } }, "node_modules/@fastify/accept-negotiator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz", - "integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==", - "license": "MIT", - "engines": { - "node": ">=14" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", + "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" }, "node_modules/@fastify/ajv-compiler": { "version": "4.0.5", @@ -607,38 +996,52 @@ } }, "node_modules/@fastify/send": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz", - "integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz", + "integrity": "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "MIT", "dependencies": { - "@lukeed/ms": "^2.0.1", + "@lukeed/ms": "^2.0.2", "escape-html": "~1.0.3", "fast-decode-uri-component": "^1.0.1", - "http-errors": "2.0.0", - "mime": "^3.0.0" + "http-errors": "^2.0.0", + "mime": "^3" } }, "node_modules/@fastify/static": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.12.0.tgz", - "integrity": "sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-9.1.3.tgz", + "integrity": "sha512-aXrYtsiryLhRxRNaxNqsn7FUISeb7rB9q4eHUPIot5aeQBLNahnz1m6thzm7JWC1poSGXS9XrX8DvuMivp2hkQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "MIT", "dependencies": { - "@fastify/accept-negotiator": "^1.0.0", - "@fastify/send": "^2.0.0", - "content-disposition": "^0.5.3", - "fastify-plugin": "^4.0.0", - "glob": "^8.0.1", - "p-limit": "^3.1.0" + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^1.0.1", + "fastify-plugin": "^5.0.0", + "fastq": "^1.17.1", + "glob": "^13.0.0" } }, - "node_modules/@fastify/static/node_modules/fastify-plugin": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", - "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==", - "license": "MIT" - }, "node_modules/@fastify/swagger": { "version": "9.7.0", "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.7.0.tgz", @@ -663,23 +1066,76 @@ } }, "node_modules/@fastify/swagger-ui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@fastify/swagger-ui/-/swagger-ui-2.1.0.tgz", - "integrity": "sha512-mu0C28kMEQDa3miE8f3LmI/OQSmqaKS3dYhZVFO5y4JdgBIPbzZj6COCoRU/P/9nu7UogzzcCJtg89wwLwKtWg==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@fastify/swagger-ui/-/swagger-ui-5.2.6.tgz", + "integrity": "sha512-OMnms0O5s9wb6wis/K5nlrAMLsgUbr1GA8uphM41IasWe3AFdgxz6r/3bA9HTxlDNUYc2FGGKeqMp3ntxmSiNA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "MIT", "dependencies": { - "@fastify/static": "^6.0.0", - "fastify-plugin": "^4.0.0", - "openapi-types": "^12.0.2", - "rfdc": "^1.3.0", - "yaml": "^2.2.2" + "@fastify/static": "^9.1.2", + "fastify-plugin": "^5.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.1" } }, - "node_modules/@fastify/swagger-ui/node_modules/fastify-plugin": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", - "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==", - "license": "MIT" + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", @@ -688,6 +1144,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@lukeed/ms": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", @@ -703,6 +1170,17 @@ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.3", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", @@ -1088,6 +1566,54 @@ "undici-types": "~7.19.0" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -1209,6 +1735,15 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -1242,6 +1777,32 @@ } } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1252,6 +1813,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -1282,10 +1862,13 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/base64-js": { "version": "1.5.1", @@ -1308,12 +1891,15 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/buffer": { @@ -1340,6 +1926,27 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1377,16 +1984,37 @@ "node": ">= 16" } }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "5.2.1" + "color-name": "~1.1.4" }, "engines": { - "node": ">= 0.6" + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cookie": { @@ -1402,6 +2030,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1429,6 +2072,46 @@ "node": ">=6" } }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1447,6 +2130,41 @@ "node": ">=6" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -1540,6 +2258,12 @@ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "license": "MIT" }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, "node_modules/fast-json-stringify": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.4.0.tgz", @@ -1699,11 +2423,22 @@ "node": ">=20" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/fsevents": { "version": "2.3.3", @@ -1740,39 +2475,83 @@ } }, "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": ">=12" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" } }, "node_modules/ieee754": { @@ -1795,17 +2574,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1821,6 +2589,125 @@ "node": ">= 10" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/it-pushable": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/it-pushable/-/it-pushable-1.4.2.tgz", @@ -1844,6 +2731,22 @@ "readable-stream": "^3.6.0" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jose": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", @@ -1902,6 +2805,49 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/light-my-request": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", @@ -1939,6 +2885,48 @@ ], "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -1946,6 +2934,15 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1956,6 +2953,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", @@ -1969,15 +2994,27 @@ } }, "node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "license": "ISC", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=10" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/ms": { @@ -2014,13 +3051,22 @@ "node": ">=14.0.0" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", "dependencies": { - "wrappy": "1" + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/openapi-types": { @@ -2048,19 +3094,37 @@ "p-defer": "^3.0.0" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", "dependencies": { - "yocto-queue": "^0.1.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=10" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/pathe": { @@ -2166,6 +3230,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/priorityqueuejs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-2.0.0.tgz", + "integrity": "sha512-19BMarhgpq3x4ccvVi8k2QpJZcymo/iFUcrhPd4V96kYGovOdTsWwy7fxChYi4QY+m2EnGBWSX9Buakz+tWNQQ==", + "license": "MIT" + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -2307,6 +3377,18 @@ "dev": true, "license": "MIT" }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2374,6 +3456,14 @@ ], "license": "BSD-3-Clause" }, + "node_modules/semaphore": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", + "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/semver": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", @@ -2398,6 +3488,29 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -2405,6 +3518,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2441,9 +3567,9 @@ "license": "MIT" }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -2465,6 +3591,110 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", @@ -2478,6 +3708,113 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/thread-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.1.0.tgz", @@ -2575,6 +3912,12 @@ "node": ">=0.6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -2793,6 +4136,22 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -2810,11 +4169,118 @@ "node": ">=8" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/yaml": { "version": "2.8.4", @@ -2831,18 +4297,6 @@ "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/dashboard/backend/src/modules/vm/repository.ts b/dashboard/backend/src/modules/vm/repository.ts index bacd966..21f6729 100644 --- a/dashboard/backend/src/modules/vm/repository.ts +++ b/dashboard/backend/src/modules/vm/repository.ts @@ -1,34 +1,35 @@ import { exec } from 'child_process'; import { promisify } from 'util'; import { hostname } from 'os'; +import { readFile } from 'fs/promises'; const execAsync = promisify(exec); -const HEALTH_SCRIPT = - '/opt/bytelyst/learning_ai_devops_tools/scripts/VMs/HostingerVM/vm-health-check.sh'; -const CLEANUP_SCRIPT = - '/opt/bytelyst/learning_ai_devops_tools/scripts/VMs/HostingerVM/vm-cleanup.sh'; -const CLEANUP_LOG = '/var/log/vm-cleanup.log'; +// Paths are env-configurable so they work both in the Docker container (via +// volume mounts) and when the backend is run directly on the host for dev. +const SCRIPTS_PATH = process.env.VM_SCRIPTS_PATH + ?? '/opt/bytelyst/learning_ai_devops_tools/scripts/VMs/HostingerVM'; +const LOG_DIR = process.env.VM_LOG_DIR ?? '/var/log'; + +const HEALTH_SCRIPT = `${SCRIPTS_PATH}/vm-health-check.sh`; +const CLEANUP_SCRIPT = `${SCRIPTS_PATH}/vm-cleanup.sh`; +const CLEANUP_LOG = `${LOG_DIR}/vm-cleanup.log`; // --------------------------------------------------------------------------- -// Health check +// Health check (vm-health-check.sh --json) // --------------------------------------------------------------------------- export async function runVmHealthCheck() { try { // Script exits 1 (WARN) or 2 (CRIT) but still emits valid JSON on stdout. - const { stdout } = await execAsync(`bash ${HEALTH_SCRIPT} --json 2>/dev/null`, { + const { stdout } = await execAsync(`bash "${HEALTH_SCRIPT}" --json 2>/dev/null`, { timeout: 30_000, }); return JSON.parse(stdout); } catch (error: any) { - // Non-zero exit — stdout may still contain valid JSON + // Non-zero exit — stdout may still contain valid JSON (WARN/CRIT result) if (error.stdout) { - try { - return JSON.parse(error.stdout); - } catch { - // fall through to error response - } + try { return JSON.parse(error.stdout); } catch { /* fall through */ } } return { timestamp: new Date().toISOString(), @@ -41,13 +42,13 @@ export async function runVmHealthCheck() { } // --------------------------------------------------------------------------- -// Cleanup log +// Cleanup log — raw tail // --------------------------------------------------------------------------- -export async function getCleanupLog(lines = 30): Promise { +export async function getCleanupLog(lines = 50): Promise { try { const { stdout } = await execAsync( - `tail -${lines} ${CLEANUP_LOG} 2>/dev/null || echo "(log not found)"`, + `tail -${lines} "${CLEANUP_LOG}" 2>/dev/null || echo "(log not found)"`, { timeout: 5_000 } ); return stdout.trim(); @@ -57,30 +58,297 @@ export async function getCleanupLog(lines = 30): Promise { } // --------------------------------------------------------------------------- -// Trigger cleanup (requires sudo — configure sudoers if needed) +// Cron status — parsed run history + next scheduled times +// --------------------------------------------------------------------------- + +export interface CronRunSummary { + timestamp: string; + mode: 'standard' | 'full'; + diskBefore: string; + diskAfter: string; + freedMB: number; + durationSecs: number; + success: boolean; + steps: string[]; + jsonSummary?: Record; +} + +export interface CronJob { + name: string; + schedule: string; + description: string; + lastRun: CronRunSummary | null; + nextRun: string | null; +} + +export async function getCronStatus(): Promise<{ jobs: CronJob[]; recentRuns: CronRunSummary[] }> { + const [rawLog, crontab] = await Promise.all([ + readFile(CLEANUP_LOG, 'utf8').catch(() => ''), + execAsync('crontab -l 2>/dev/null').then(r => r.stdout).catch(() => ''), + ]); + + const recentRuns = parseCleanupLog(rawLog); + const jobs = buildJobList(crontab, recentRuns); + return { jobs, recentRuns: recentRuns.slice(0, 20) }; +} + +function parseCleanupLog(raw: string): CronRunSummary[] { + const runs: CronRunSummary[] = []; + // Runs are delimited by [START] lines + const blocks = raw.split(/\[START\]/); + for (const block of blocks.slice(1)) { + try { + const startLine = block.match(/\[(\d{4}-\d{2}-\d{2}T[\d:Z]+)\] mode=(\w+)/); + if (!startLine) continue; + const timestamp = startLine[1]; + const mode = startLine[2] === 'full' ? 'full' : 'standard'; + + const diskLine = block.match(/\[DISK\] before=([^\s]+) after=([^\s]+)/); + const endLine = block.match(/\[END\]/); + const cmdLines = [...block.matchAll(/\[CMD\] (.+)/g)].map(m => m[1]); + const jsonMatch = block.match(/\[JSON\] ({.+})/); + + // Compute freed MB from disk "used" before/after (e.g. " 70G 123G 37%") + let freedMB = 0; + let diskBefore = '', diskAfter = ''; + if (diskLine) { + diskBefore = diskLine[1].trim(); + diskAfter = diskLine[2].trim(); + const gbBefore = parseFloat(diskLine[1].match(/([\d.]+)G/)?.[1] ?? '0'); + const gbAfter = parseFloat(diskLine[2].match(/([\d.]+)G/)?.[1] ?? '0'); + freedMB = Math.round((gbBefore - gbAfter) * 1024); + } + + // Rough duration: time from start to end + const startTs = new Date(timestamp).getTime(); + const endTs = endLine + ? (() => { + const m = block.slice(block.indexOf('[END]') - 28, block.indexOf('[END]') - 2); + return new Date(m.match(/\d{4}-\d{2}-\d{2}T[\d:Z]+/)?.[0] ?? timestamp).getTime(); + })() + : startTs; + const durationSecs = Math.round((endTs - startTs) / 1000); + + runs.push({ + timestamp, + mode, + diskBefore, + diskAfter, + freedMB, + durationSecs: isNaN(durationSecs) ? 0 : durationSecs, + success: !!endLine, + steps: cmdLines, + jsonSummary: jsonMatch ? JSON.parse(jsonMatch[1]) : undefined, + }); + } catch { + // Skip malformed blocks + } + } + return runs.reverse(); // most recent first +} + +function buildJobList(crontab: string, runs: CronRunSummary[]): CronJob[] { + const managed = crontab.match(/# bytelyst-vm-maintenance[\s\S]+/m)?.[0] ?? ''; + + const defs: Array<{ name: string; schedule: string; description: string; mode?: string }> = [ + { name: 'build-cache-prune', schedule: '0 3 * * *', description: 'Daily build cache prune' }, + { name: 'weekly-cleanup', schedule: '0 2 * * 0', description: 'Weekly standard cleanup' }, + { name: 'monthly-full', schedule: '0 1 1 * *', description: 'Monthly full cleanup', mode: 'full' }, + { name: 'health-check', schedule: '0 7 * * *', description: 'Daily health check + Telegram alert' }, + ]; + + return defs.map(def => { + const matchingRun = def.mode === 'full' + ? runs.find(r => r.mode === 'full') + : runs.find(r => r.mode === 'standard'); + + const nextRun = computeNextRun(def.schedule); + return { ...def, lastRun: matchingRun ?? null, nextRun }; + }); +} + +/** Very lightweight cron next-run calculator (handles standard 5-field expressions) */ +function computeNextRun(expr: string): string { + const [min, hr, dom, , dow] = expr.split(' '); + const now = new Date(); + const next = new Date(now); + next.setSeconds(0, 0); + next.setMinutes(parseInt(min)); + next.setHours(parseInt(hr)); + + if (dom === '*' && dow !== '*') { + // Weekly: advance to correct day-of-week + const targetDow = parseInt(dow); + const dayDiff = (targetDow - next.getDay() + 7) % 7 || 7; + next.setDate(next.getDate() + dayDiff); + } else if (dom !== '*') { + // Monthly: advance to correct day-of-month + next.setDate(parseInt(dom)); + if (next <= now) next.setMonth(next.getMonth() + 1); + } else { + // Daily: just advance to tomorrow if already passed today + if (next <= now) next.setDate(next.getDate() + 1); + } + + return next.toISOString(); +} + +// --------------------------------------------------------------------------- +// Trigger cleanup (container runs as root — no sudo needed) // --------------------------------------------------------------------------- export async function runVmCleanup( - mode: 'weekly' | 'monthly' | 'dry-run' + mode: 'weekly' | 'monthly' | 'dry-run', ): Promise<{ success: boolean; output: string }> { const args = - mode === 'monthly' - ? '--full --quiet' - : mode === 'dry-run' - ? '--dry-run' - : '--quiet'; + mode === 'monthly' ? '--full --quiet' : + mode === 'dry-run' ? '--dry-run' : + '--quiet'; try { const { stdout, stderr } = await execAsync( - `sudo bash ${CLEANUP_SCRIPT} ${args} 2>&1`, - { timeout: 120_000 } + `bash "${CLEANUP_SCRIPT}" ${args} 2>&1`, + { timeout: 120_000 }, ); return { success: true, output: (stdout + stderr).trim() }; } catch (error: any) { - const out = (error.stdout || '') + (error.stderr || ''); - return { - success: false, - output: out.trim() || String(error.message || error), - }; + const out = ((error.stdout ?? '') + (error.stderr ?? '')).trim(); + return { success: false, output: out || String(error.message ?? error) }; + } +} + +// --------------------------------------------------------------------------- +// Unhealthy containers (docker inspect via shell — no Docker SDK needed) +// --------------------------------------------------------------------------- + +export interface UnhealthyContainer { + name: string; + status: string; + restartCount: number; + lastHealthLogs: string[]; + unhealthySince: string | null; +} + +export async function getUnhealthyContainers(): Promise { + try { + const { stdout } = await execAsync( + `docker ps --filter health=unhealthy --format '{{.Names}}' 2>/dev/null`, + { timeout: 10_000 }, + ); + const names = stdout.trim().split('\n').filter(Boolean); + if (!names.length) return []; + + const results = await Promise.all(names.map(async name => { + try { + const { stdout: raw } = await execAsync( + `docker inspect "${name}" 2>/dev/null`, + { timeout: 5_000 }, + ); + const data = JSON.parse(raw)?.[0]; + const health = data?.State?.Health ?? {}; + const logs: string[] = (health.Log ?? []) + .slice(-3) + .map((l: any) => l.Output?.trim() ?? ''); + const unhealthySince = health.Log?.[0]?.Start ?? null; + + return { + name, + status: data?.State?.Status ?? 'unknown', + restartCount: data?.RestartCount ?? 0, + lastHealthLogs: logs, + unhealthySince, + } satisfies UnhealthyContainer; + } catch { + return { name, status: 'unknown', restartCount: 0, lastHealthLogs: [], unhealthySince: null }; + } + })); + + return results; + } catch { + return []; + } +} + +export async function restartContainer(name: string): Promise<{ success: boolean; message: string }> { + // Validate name — only allow alphanumeric, dash, underscore + if (!/^[\w-]+$/.test(name)) { + return { success: false, message: 'Invalid container name' }; + } + try { + await execAsync(`docker restart "${name}"`, { timeout: 30_000 }); + return { success: true, message: `${name} restarted` }; + } catch (error: any) { + return { success: false, message: String(error.stderr || error.message || error) }; + } +} + +// --------------------------------------------------------------------------- +// Ollama models +// --------------------------------------------------------------------------- + +export interface OllamaModel { + name: string; + sizeGB: number; + modifiedAt: string; +} + +export interface OllamaRunning { + name: string; + sizeGB: number; + processor: string; + expiresAt: string; +} + +// Ollama REST API base — host-gateway resolves to the Docker host, +// where ollama serve listens on port 11434. +const OLLAMA_BASE = process.env.OLLAMA_BASE_URL ?? 'http://host-gateway:11434'; + +async function ollamaFetch(path: string, opts?: RequestInit): Promise { + const res = await fetch(`${OLLAMA_BASE}${path}`, { + signal: AbortSignal.timeout(10_000), + ...opts, + }); + if (!res.ok) throw new Error(`Ollama ${path}: ${res.status}`); + return res.json(); +} + +export async function getOllamaModels(): Promise<{ models: OllamaModel[]; running: OllamaRunning[] }> { + try { + const [tagsData, psData] = await Promise.all([ + ollamaFetch('/api/tags').catch(() => ({ models: [] })), + ollamaFetch('/api/ps').catch(() => ({ models: [] })), + ]); + + const models = ((tagsData as any).models ?? []).map((m: any) => ({ + name: m.name ?? '', + sizeGB: parseFloat(((m.size ?? 0) / 1e9).toFixed(2)), + modifiedAt: m.modified_at ?? '', + })); + + const running = ((psData as any).models ?? []).map((m: any) => ({ + name: m.name ?? '', + sizeGB: parseFloat(((m.size ?? 0) / 1e9).toFixed(2)), + processor: m.details?.families?.join(', ') ?? '', + expiresAt: m.expires_at ?? '', + })); + + return { models, running }; + } catch { + return { models: [], running: [] }; + } +} + +export async function unloadOllamaModel(name: string): Promise<{ success: boolean; message: string }> { + if (!/^[\w.:\-/]+$/.test(name)) return { success: false, message: 'Invalid model name' }; + try { + // Unload by setting keep_alive to 0 + await ollamaFetch('/api/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model: name, keep_alive: 0 }), + }); + return { success: true, message: `${name} unloaded` }; + } catch (error: any) { + return { success: false, message: String(error.message ?? error) }; } } diff --git a/dashboard/backend/src/modules/vm/routes.ts b/dashboard/backend/src/modules/vm/routes.ts index a7fe693..8697472 100644 --- a/dashboard/backend/src/modules/vm/routes.ts +++ b/dashboard/backend/src/modules/vm/routes.ts @@ -1,47 +1,130 @@ import type { FastifyInstance } from 'fastify'; import { requireAdmin } from '../../lib/auth.js'; -import { runVmHealthCheck, getCleanupLog, runVmCleanup } from './repository.js'; -import { VmCleanupParamsSchema } from './types.js'; +import { + runVmHealthCheck, + getCleanupLog, + runVmCleanup, + getCronStatus, + getUnhealthyContainers, + restartContainer, + getOllamaModels, + unloadOllamaModel, +} from './repository.js'; +import { VmCleanupParamsSchema, VmContainerRestartParamsSchema } from './types.js'; export async function vmRoutes(fastify: FastifyInstance) { - // GET /api/vm/health — run vm-health-check.sh --json and return result (admin only) + + // ── Health check ────────────────────────────────────────────────────────── + + // GET /api/vm/health — run vm-health-check.sh --json fastify.get('/vm/health', { preHandler: async (req) => requireAdmin(req), }, async (_req, reply) => { try { - const result = await runVmHealthCheck(); - return reply.send(result); + return reply.send(await runVmHealthCheck()); } catch (error) { fastify.log.error(error, 'VM health check failed'); return reply.code(500).send({ error: 'VM health check failed' }); } }); - // GET /api/vm/cleanup-log — tail the cleanup log (admin only) + // ── Cleanup log (raw tail) ───────────────────────────────────────────────── + + // GET /api/vm/cleanup-log?lines=50 fastify.get('/vm/cleanup-log', { preHandler: async (req) => requireAdmin(req), }, async (req, reply) => { try { - const lines = Number((req.query as any).lines) || 30; - const log = await getCleanupLog(lines); - return reply.send({ log }); + const lines = Math.min(Number((req.query as any).lines) || 50, 500); + return reply.send({ log: await getCleanupLog(lines) }); } catch (error) { fastify.log.error(error, 'Failed to read cleanup log'); return reply.code(500).send({ error: 'Failed to read cleanup log' }); } }); - // POST /api/vm/cleanup — trigger vm-cleanup.sh (admin only) + // ── Cron status (parsed history + schedule) ─────────────────────────────── + + // GET /api/vm/cron-status + fastify.get('/vm/cron-status', { + preHandler: async (req) => requireAdmin(req), + }, async (_req, reply) => { + try { + return reply.send(await getCronStatus()); + } catch (error) { + fastify.log.error(error, 'Failed to get cron status'); + return reply.code(500).send({ error: 'Failed to get cron status' }); + } + }); + + // ── Cleanup trigger ─────────────────────────────────────────────────────── + + // POST /api/vm/cleanup { mode: 'weekly' | 'monthly' | 'dry-run' } fastify.post('/vm/cleanup', { preHandler: async (req) => requireAdmin(req), }, async (req, reply) => { try { const params = VmCleanupParamsSchema.parse(req.body); - const result = await runVmCleanup(params.mode); - return reply.send(result); + return reply.send(await runVmCleanup(params.mode)); } catch (error: any) { fastify.log.error(error, 'VM cleanup failed'); return reply.code(500).send({ error: error.message || 'VM cleanup failed' }); } }); + + // ── Unhealthy containers ────────────────────────────────────────────────── + + // GET /api/vm/containers/unhealthy + fastify.get('/vm/containers/unhealthy', { + preHandler: async (req) => requireAdmin(req), + }, async (_req, reply) => { + try { + return reply.send({ containers: await getUnhealthyContainers() }); + } catch (error) { + fastify.log.error(error, 'Failed to get unhealthy containers'); + return reply.code(500).send({ error: 'Failed to get unhealthy containers' }); + } + }); + + // POST /api/vm/containers/:name/restart + fastify.post('/vm/containers/:name/restart', { + preHandler: async (req) => requireAdmin(req), + }, async (req, reply) => { + try { + const { name } = VmContainerRestartParamsSchema.parse(req.params); + const result = await restartContainer(name); + return reply.code(result.success ? 200 : 400).send(result); + } catch (error: any) { + fastify.log.error(error, 'Container restart failed'); + return reply.code(500).send({ error: error.message || 'Container restart failed' }); + } + }); + + // ── Ollama / LLM models ─────────────────────────────────────────────────── + + // GET /api/vm/ollama/models + fastify.get('/vm/ollama/models', { + preHandler: async (req) => requireAdmin(req), + }, async (_req, reply) => { + try { + return reply.send(await getOllamaModels()); + } catch (error) { + fastify.log.error(error, 'Failed to get Ollama models'); + return reply.code(500).send({ error: 'Failed to get Ollama models' }); + } + }); + + // DELETE /api/vm/ollama/models/:name — unload running model + fastify.delete('/vm/ollama/models/:name', { + preHandler: async (req) => requireAdmin(req), + }, async (req, reply) => { + try { + const name = decodeURIComponent((req.params as any).name ?? ''); + const result = await unloadOllamaModel(name); + return reply.code(result.success ? 200 : 400).send(result); + } catch (error: any) { + fastify.log.error(error, 'Failed to unload Ollama model'); + return reply.code(500).send({ error: error.message || 'Unload failed' }); + } + }); } diff --git a/dashboard/backend/src/modules/vm/types.ts b/dashboard/backend/src/modules/vm/types.ts index fabc7e7..5362c15 100644 --- a/dashboard/backend/src/modules/vm/types.ts +++ b/dashboard/backend/src/modules/vm/types.ts @@ -29,3 +29,10 @@ export const VmCleanupResultSchema = z.object({ output: z.string(), }); export type VmCleanupResult = z.infer; + +// ── Container restart ───────────────────────────────────────────────────────── + +export const VmContainerRestartParamsSchema = z.object({ + name: z.string().regex(/^[\w-]+$/, 'Invalid container name'), +}); +export type VmContainerRestartParams = z.infer; diff --git a/dashboard/docker-compose.yml b/dashboard/docker-compose.yml index bb99431..1200572 100644 --- a/dashboard/docker-compose.yml +++ b/dashboard/docker-compose.yml @@ -22,11 +22,27 @@ services: container_name: devops-backend env_file: - backend/.env + environment: + - VM_SCRIPTS_PATH=/vm-scripts/VMs/HostingerVM + - VM_LOG_DIR=/host-logs ports: - '4004:4004' networks: - default - platform_net + volumes: + # Read-only access to VM management scripts + - /opt/bytelyst/learning_ai_devops_tools/scripts:/vm-scripts:ro + # Read-write access to VM log files (cleanup + health-check write here) + - /var/log/vm-cleanup.log:/host-logs/vm-cleanup.log + - /var/log/vm-health-check.log:/host-logs/vm-health-check.log + - /var/log/docker-watchdog.log:/host-logs/docker-watchdog.log + # Docker socket — allows running docker commands against the host daemon + # (same pattern as Portainer/cAdvisor; container already runs as root) + - /var/run/docker.sock:/var/run/docker.sock + extra_hosts: + # Reach the host for Ollama API (port 11434) and host-only services + - "host-gateway:host-gateway" restart: unless-stopped healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:4004/health'] diff --git a/docs/VM_OBSERVABILITY_ROADMAP.md b/docs/VM_OBSERVABILITY_ROADMAP.md new file mode 100644 index 0000000..ae5ed71 --- /dev/null +++ b/docs/VM_OBSERVABILITY_ROADMAP.md @@ -0,0 +1,399 @@ +# VM Observability & Control Roadmap — v2 +**Status:** Draft — Pending Approval +**Last updated:** 2026-05-27 +**Scope:** `srv1491630` (Hostinger VM) + DevOps Dashboard (`devops.bytelyst.com`) +**Reviewed:** Yes — v1 audited against live system; 11 issues corrected (see change log at bottom) + +--- + +## Current State Snapshot + +| Layer | What exists today | Verified gap | +|---|---|---| +| **Health check** | `vm-health-check.sh` — disk, load, RAM, swap, Docker | No steal time metric; no per-container detail | +| **Cleanup** | `vm-cleanup.sh` — build cache, images, logs, apt, pnpm, HOLD | Runs silently; no structured outcome record | +| **Cron** | 4 scheduled jobs (daily / weekly / monthly) | No execution history; no "last ran / freed X" | +| **Dashboard /vm** | Health check + cleanup log tail + trigger button | **VM module is non-functional** — container has no host volume mounts; all backend calls to host scripts fail silently | +| **Dashboard /system** | CPU, RAM, disk, Docker stats | Missing steal %, container detail, unhealthy drill-down | +| **Prometheus stack** | Prometheus + cAdvisor + node-exporter + Loki — ~2 weeks history | **No Grafana**; trend data exists but no UI to query it | +| **Alerting** | Telegram on WARN/CRIT at 07:00 UTC | No steal time alert; no weekly digest; no cron failure alert | +| **Container restart** | 38/39 containers have `unless-stopped` | `unless-stopped` restarts on *process exit only* — does NOT react to health check failures. 7 containers running but unhealthy (process alive, health endpoint dead) | +| **LLMs (Ollama)** | 9 models on disk; `qwen2.5-coder:1.5b` currently loaded (1.1 GB, 100% CPU) | No RAM impact warning before loading; no dashboard visibility | +| **I/O anomaly** | `invttrdg-backend` writing ~22 GB/day to block storage | Unexplained — no alert, no investigation | + +--- + +## Architectural Decisions (settle these before building) + +### A. Trend chart data source +**Options:** +- ✅ **Query existing Prometheus** from DevOps dashboard (recommended) — data already there, no new store needed. Add Prometheus query endpoints to dashboard backend, render with a chart library. +- ➕ **Add Grafana container** alongside Prometheus — purpose-built for metrics UI, out-of-box dashboards. Extra 80–150 MB RAM. +- ❌ **New Cosmos DB vm-metrics container** — redundant with Prometheus; wrong tool for time-series. + +**Recommendation:** Query Prometheus from the dashboard for Phase 4.2 charts (keeps everything in one UI). Add Grafana in Phase 5 only if dashboard charts feel limiting. + +### B. Dashboard → host script execution +The `devops-backend` container currently has **no host volume mounts** and **no sudoers entry**. Phase 3.2 "Run cleanup from dashboard" requires one of: +- ✅ **Mount host script + Docker socket** into devops-backend (simplest, lowest risk) +- ➕ **Thin host-side agent** (systemd socket-activated, receives commands via Unix socket) +- ❌ **SSH from container to host** — unnecessary complexity + +**Recommendation:** Mount `/opt/bytelyst/learning_ai_devops_tools/scripts` read-only + `/var/log` for log reading into devops-backend. Add sudoers entry for the cleanup script only. + +--- + +## Phase 0 — Fix Broken Foundations *(Day 1–2, prerequisite for all UI phases)* + +These are not new features — they are bugs in the current system. + +#### 0.1 Fix devops-backend VM module (host volume mounts) +**Problem:** `GET /api/vm/health`, `GET /api/vm/cleanup-log`, `POST /api/vm/cleanup` all fail because the container has no access to the host filesystem. +**Fix:** Update `docker-compose.yml` for devops-backend: +```yaml +volumes: + - /opt/bytelyst/learning_ai_devops_tools/scripts:/scripts:ro + - /var/log/vm-cleanup.log:/var/log/vm-cleanup.log:ro + - /var/log/vm-health-check.log:/var/log/vm-health-check.log:ro +``` +Update `repository.ts` to use `/scripts/VMs/HostingerVM/vm-cleanup.sh` path, or use env var `VM_SCRIPTS_PATH`. +Add sudoers entry: `nobody ALL=(ALL) NOPASSWD: /scripts/VMs/HostingerVM/vm-cleanup.sh` +**Risk:** Low. Read-only mounts for scripts, append-only for logs. +**Validates:** Run `curl http://localhost:4004/api/vm/health` and confirm JSON response. + +#### 0.2 Add logrotate entry for new log files +**Problem:** `/var/log/vm-cleanup.log` and `/var/log/vm-health-check.log` have no rotation policy. Will grow unbounded. +**Fix:** Create `/etc/logrotate.d/bytelyst-vm`: +``` +/var/log/vm-cleanup.log /var/log/vm-health-check.log /var/log/docker-watchdog.log { + weekly + rotate 8 + compress + delaycompress + missingok + notifempty + create 0644 root root +} +``` + +#### 0.3 Investigate `invttrdg-backend` I/O anomaly +**Problem:** 22.2 GB block writes in 13 hours (~1.7 GB/hr). At this rate: 40 GB/day, will fill the 123 GB free disk in ~3 days of heavy trading activity. +**Fix path:** Check what's being written (WAL logs? tick data? verbose debug logging?). Likely a log level or persistence config issue. Add disk usage alert specific to this container. +**Risk of not fixing:** Disk fills up, all services go down. + +--- + +## Phase 1 — Observability Gaps *(Week 1)* + +Read-only additions to existing scripts and the `/vm` dashboard page. + +#### 1.1 Cron Job Execution History Panel +**Where:** Dashboard `/vm` page — new "Maintenance Schedule" card +**What:** Add `GET /api/vm/cron-status` endpoint that: +1. Parses crontab entries for the 4 managed jobs (look for `# bytelyst-vm-maintenance` block) +2. Parses `/var/log/vm-cleanup.log` into structured run objects: `{ timestamp, mode, diskBefore, diskAfter, freedMB, steps[], success }` +3. Calculates next run from cron expression + +**UI:** Table — job name | schedule | last run | freed | status | next run. Expandable row shows step-by-step log. +**Dependency:** Requires Phase 0.1 (volume mount for log access). + +#### 1.2 CPU Steal Time Metric +**Where:** `vm-health-check.sh` + dashboard `/vm` health cards +**What:** Sample `/proc/stat` twice 1 second apart, compute steal %: +```bash +read_steal() { awk '/^cpu /{print $9" "$2+$3+$4+$5+$6+$7+$8+$9+$10}' /proc/stat; } +s1=$(read_steal); sleep 1; s2=$(read_steal) +steal_pct=$(awk -v s1="$s1" -v s2="$s2" 'BEGIN{ + split(s1,a," "); split(s2,b," ") + delta_steal=b[1]-a[1]; delta_total=b[2]-a[2] + printf "%.1f", (delta_steal/delta_total)*100 +}') +``` +Thresholds: `> 5%` = WARN, `> 15%` = CRIT. +**Why:** Currently at **8.2%** — silently degrading every API response and LLM inference call. +**Dependency:** None. Self-contained script change. + +#### 1.3 Unhealthy Container Detail Panel +**Where:** Dashboard `/vm` — expand container health card +**What:** New `GET /api/vm/containers/unhealthy` endpoint: +- Container name, `unhealthy` since (parse `docker inspect .State.Health.Log[0].Start`) +- Last 3 health check log lines +- Current restart count + +**UI:** Expandable per-container row with one-click restart button (calls existing or new `POST /api/vm/containers/:name/restart`). +**Dependency:** Requires Phase 0.1. + +#### 1.4 Swap Pressure Indicator +**Where:** `vm-health-check.sh` + dashboard +**What:** Add `SwapCached` as secondary metric. High SwapCached relative to SwapUsed = system was recently under pressure even if swap looks ok now. Surface in daily Telegram alert even when overall = WARN not CRIT. +**Threshold change:** Current `SWAP_USED_WARN_GB=1` triggers today (1.4 GB in use). Consider raising to `1.5` to reduce noise while keeping the `SwapCached > 200MB` as an early warning signal. + +--- + +## Phase 2 — Self-Healing Automation *(Week 2)* + +Scripts that fix known recurring issues automatically. + +#### 2.1 Health-Check-Aware Container Watchdog +**Why the existing policy isn't enough:** All 38 containers already have `unless-stopped`. That policy restarts on *container process exit* only. When the web server process is alive but the health check endpoint returns `Connection refused`, Docker marks the container `unhealthy` but **does not restart it** — it keeps running indefinitely broken. +**Fix:** Systemd timer `docker-health-watchdog.timer` (runs every 10 minutes): +```bash +#!/bin/bash +# /usr/local/bin/docker-health-watchdog.sh +UNHEALTHY=$(docker ps --filter health=unhealthy --format '{{.Names}}') +for container in $UNHEALTHY; do + # Only restart if unhealthy for at least 3 consecutive checks (30 min) + failures=$(docker inspect "$container" | \ + python3 -c "import json,sys; h=json.load(sys.stdin)[0]['State']['Health']['Log']; \ + print(sum(1 for l in h[-3:] if l['ExitCode']!=0))") + if [[ "$failures" -eq 3 ]]; then + docker restart "$container" + echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Auto-restarted: $container (unhealthy 3x)" \ + >> /var/log/docker-watchdog.log + # Telegram notify (reads token from $HERMES_HOME/.env) + fi +done +``` +**Safety:** Never restarts a container that just became unhealthy (3-check cooldown). Logs every restart. Only targets health-check failures, not intentionally stopped containers. +**Rollback:** `systemctl disable docker-health-watchdog.timer` + +#### 2.2 Fix `hermes-root-backup` Git Diverge +**Current failure:** Git fast-forward fails every ~10 minutes since 16:25 today (~30+ silent failures). +**Fix:** Patch the backup script to handle diverge gracefully: +```bash +if ! git pull --ff-only 2>/dev/null; then + # Log the diverge + git log --oneline -3 HEAD > /tmp/hermes-diverge-before.txt + git log --oneline -3 origin/main >> /tmp/hermes-diverge-before.txt + # Try rebase first (preserves local commits if intentional) + if ! git pull --rebase 2>/dev/null; then + # If rebase fails, reset to origin (backup is the source of truth) + git reset --hard origin/main + notify_telegram "⚠️ hermes-root-backup: diverged branch reset to origin/main" + fi +fi +``` +**Risk:** `git reset --hard` loses any local-only commits on the backup repo. Acceptable here because the backup script's job is to *push to* origin — local commits shouldn't exist. Add a pre-check: if local commits exist that aren't on origin, alert instead of resetting. + +#### 2.3 Container Memory Limits +**Validated against actual RSS data (Phase 2 data collected 2026-05-27):** + +| Category | Current RSS | Proposed Limit | Reservation | Notes | +|---|---|---|---|---| +| Next.js web frontends | 17–37 MB | `256m` | `64m` | 7× headroom for webpack spikes | +| Node/Fastify backends | 20–67 MB | `384m` | `128m` | Allows burst for LLM calls | +| `invttrdg-backend` | 107 MB | `512m` | `256m` | High I/O service; watch after 0.3 | +| `trading-backend` | 92 MB | `512m` | `256m` | Active algo trading service | +| `platform-service` | 66 MB | `384m` | `128m` | Shared auth/platform layer | +| CosmosDB emulator | 145 MB | `1g` | `512m` | Can spike on write bursts | +| Prometheus | 57 MB | `256m` | `128m` | Stable but grows with series | +| Loki | 53 MB | `256m` | `128m` | Log ingestion can spike | +| Caddy | 27 MB | `128m` | `64m` | Proxy, very stable | +| Valkey (Redis) | 3.5 MB | `128m` | `32m` | Cache, tiny | +| Gitea | 79 MB | `512m` | `256m` | Git operations can spike | +| Ollama | 130 MB idle | **No limit** | — | Must accommodate model load (up to 8 GB) | + +**Rollout strategy:** +1. Run `docker stats` baseline for 24h to confirm no container spikes beyond proposed limits +2. Apply limits per stack in docker-compose files (not `docker update` — recreate on next deploy) +3. Monitor for OOMKill events: `dmesg | grep -i oom` for 48h after rollout +4. **Never set limits on Ollama** — model loading is unpredictable and limits would kill inference + +--- + +## Phase 3 — Dashboard Control Plane *(Weeks 3–4)* + +**Prerequisite for all Phase 3 items:** Phase 0.1 (host volume mount) must be complete. + +#### 3.1 VM Score Card (Automated) +**Where:** Dashboard `/vm` — top summary widget, auto-refreshes every 5 min +**Scoring algorithm (0–100):** +``` +CPU efficiency: 20 pts (steal < 2% = 20, < 5% = 15, < 10% = 10, ≥ 10% = 5) +Memory pressure: 20 pts (available > 6 GB = 20, > 3 GB = 15, > 1 GB = 5, else = 0) +Disk health: 15 pts (< 40% used = 15, < 55% = 10, < 70% = 5, else = 0) +Service health: 20 pts (0 unhealthy = 20, 1–2 = 15, 3–5 = 8, 6+ = 2) +Maintenance hygiene: 15 pts (last cleanup < 7 days + freed > 0 = 15, < 30 days = 8, else = 0) +LLM readiness: 10 pts (> 8 GB free RAM = 10, > 4 GB = 7, > 2 GB = 4, else = 1) +``` +Score = sum. Display as gauge. Each dimension clickable to drill into its data. +**Dependencies:** Phase 1.2 (steal time in health check output). + +#### 3.2 Cron Schedule & History Panel +**Where:** Dashboard `/vm` — "Maintenance" tab +**What:** +- Live table: 4 cron jobs × (last run, result, freed MB, next scheduled, "Run now" button) +- Last 30 cleanup runs as a sparkline: date vs MB freed +- One-click trigger for weekly / monthly / dry-run + +**Backend endpoint:** `GET /api/vm/cron-status` — parse structured log + crontab +**Dependency:** Phase 0.1 (volume mount), Phase 1.1 (structured log parser). + +#### 3.3 Container Management Panel +**Where:** Dashboard `/vm` — "Containers" tab +**What:** +- Full list: name, stack, health status, uptime, CPU %, RAM, restart count +- Filter chips: All | Unhealthy | No Memory Limit | By stack +- Per-container: Restart, View last 50 log lines, Show health check history +- Bulk: "Restart all unhealthy" with confirmation modal + +**New backend endpoints:** `GET /api/vm/containers`, `POST /api/vm/containers/:name/restart`, `GET /api/vm/containers/:name/logs` + +#### 3.4 Ollama / LLM Panel +**Where:** Dashboard `/vm` — "Models" tab +**What:** +- Models list: name, size, last used timestamp +- Currently loaded (from `ollama ps`): model name, RAM used, CPU %, expires in +- RAM visualisation bar: [used by system] [model if loaded] [free] +- Warning banner: "Loading llama3.2-vision (7.8 GB) will leave ~1.2 GB free — swap pressure likely" +- Load / Unload model buttons + +**Backend endpoints:** `GET /api/vm/ollama/models`, `POST /api/vm/ollama/load`, `DELETE /api/vm/ollama/unload` +**Note:** `qwen2.5-coder:1.5b` is currently loaded — confirmed via `ollama ps`. + +--- + +## Phase 4 — Trend Analysis *(Weeks 5–6)* + +**Key architectural note:** Prometheus + cAdvisor + node-exporter are **already running and storing ~2 weeks of metrics history** including steal time, disk I/O, memory, container CPU/RAM. Do NOT create a separate Cosmos DB store. Query Prometheus directly. + +#### 4.1 Prometheus Query Endpoints in Dashboard Backend +**Where:** New `GET /api/vm/metrics/trend` endpoint group +**What:** Proxy queries to internal Prometheus (http://prometheus:9090 within Docker network): +``` +/api/vm/metrics/trend/disk?range=7d → disk usage % over time +/api/vm/metrics/trend/memory?range=7d → available RAM + swap used over time +/api/vm/metrics/trend/steal?range=7d → CPU steal % over time (once 1.2 is deployed) +/api/vm/metrics/trend/containers?range=7d → unhealthy container count over time +/api/vm/metrics/trend/io?range=7d → block write rate (flag invttrdg spikes) +``` +**Note:** `devops-backend` is on `dashboard_default` network, Prometheus is on `learning_ai_common_plat_default`. Either add devops-backend to Prometheus network, or expose Prometheus on a host port (internal only, not via Caddy). + +#### 4.2 Trend Charts on Dashboard +**Where:** Dashboard `/vm` — collapsible "Trends" section below score card +**What (7-day / 30-day toggle):** +- Disk % over time + linear projection line → "estimated to hit 55% warning in X days" +- Swap used over time (detect slow memory leak) +- CPU steal % over time (detect host degradation trend) +- Unhealthy container count per day +- Block write rate: flag days with `invttrdg-backend` anomalies + +**Library recommendation:** Recharts (already likely in the Next.js project) or lightweight Chart.js wrapper. + +#### 4.3 Weekly Digest (Telegram) +**Where:** New cron job — Monday 08:00 UTC — `vm-cleanup.sh --weekly-digest` +**What:** +``` +📊 Weekly VM Digest — srv1491630 +Week ending 2026-06-01 + +🖥 CPU Steal: 8.2% avg ⚠️ (host contention — escalate if > 10%) +💾 Disk: 37% (freed 257 MB this week via cleanup) +🧠 RAM: 10 GB free avg ✓ +🔄 Swap peak: 1.4 GB ⚠️ +🐳 Containers: 7 unhealthy (action required) +🤖 LLMs run: qwen2.5-coder:1.5b (3 sessions this week) +🧹 Cleanups: 1 standard, 0 full +📅 Next full: 2026-06-01 + +Top action: Restart 7 unhealthy web containers +``` +**Dependency:** Phase 4.1 (needs Prometheus for weekly averages), Phase 1.2 (steal metric must be in Prometheus). + +--- + +## Phase 5 — Advanced / Backlog + +| Item | Description | Trigger condition | +|---|---|---| +| **Add Grafana** | Container alongside Prometheus for richer dashboards; pre-built node-exporter dashboards available | Phase 4 charts feel limited | +| **Deployment ↔ health correlation** | Mark deploys on trend charts; correlate health dips to specific releases | After Phase 4.2 exists | +| **Multi-VM support** | Extend all above to aggregate across VMs | Adding second VM | +| **`invttrdg-backend` write audit** | Persistent investigation: what generates 22 GB/day of block writes? Add per-container I/O alert | After Phase 0.3 | +| **Chaos validation** | Monthly: watchdog stops a test container, verify restart within 10 min, report result | After Phase 2.1 | +| **Ollama GPU readiness check** | Detect GPU availability, surface in LLM panel as "GPU: none — inference will be slow" | Before adding large models | +| **Container image freshness** | Alert when container is running image > 30 days old (not rebuilt) | When deploy pipeline matures | +| **Cost attribution** | Tag containers by product (trading, notes, clock...) — RAM/CPU cost per product | When billing needed | +| **Backup health tracking** | `hermes-root-backup` and `uma-hermes-backup` results surfaced in dashboard | After Phase 2.2 | + +--- + +## Implementation Order + +``` +Day 1–2 Phase 0 ── Fix broken foundations (VM module, logrotate, I/O investigation) + ⚠️ MUST complete before any Phase 3 dashboard work + +Week 1 Phase 1 ── Observability (steal metric, cron history, unhealthy detail, swap) + 1.2 (steal) → unblocks 3.1 (score card) + 1.1 (cron log format) → unblocks 3.2 (cron panel) + +Week 2 Phase 2 ── Self-healing (watchdog, hermes-backup fix, memory limits) + 2.1 requires: logrotate entry (Phase 0.2) + 2.3 requires: 24h baseline observation first + +Weeks 3–4 Phase 3 ── Dashboard control (score card, cron panel, containers, Ollama) + All require: Phase 0.1 (host volume mount) + 3.1 requires: Phase 1.2 deployed + 3.2 requires: Phase 1.1 deployed + +Weeks 5–6 Phase 4 ── Trend analysis (Prometheus queries, charts, weekly digest) + 4.1 requires: devops-backend on same Docker network as Prometheus + 4.2 requires: Phase 4.1 + 4.3 requires: Phase 4.1 + Phase 1.2 + +Backlog Phase 5 ── Advanced items, trigger-based +``` + +--- + +## Success Criteria (how to know each phase is done) + +| Phase | Done when… | +|---|---| +| 0.1 | `curl localhost:4004/api/vm/health` returns valid JSON with disk/load/swap data | +| 0.2 | `logrotate -d /etc/logrotate.d/bytelyst-vm` exits 0; logs present in `/var/log` | +| 0.3 | Root cause of 22 GB/day writes identified + alert configured | +| 1.1 | Dashboard `/vm` shows "Last cleanup: [date], freed [MB]" parsed from log | +| 1.2 | `vm-health-check.sh` includes steal % in output; Telegram sends steal alert at > 5% | +| 1.3 | Dashboard shows each unhealthy container's last health log + restart button works | +| 2.1 | Watchdog restarts an intentionally-broken test container within 30 min | +| 2.2 | `hermes-root-backup` runs 10 times without failure after fix deployed | +| 2.3 | All containers show memory limits in `docker inspect`; 48h with 0 OOMKill events | +| 3.1 | Score card renders live score; each dimension links to its detail | +| 4.1 | `/api/vm/metrics/trend/disk?range=7d` returns valid Prometheus time-series JSON | +| 4.3 | Telegram receives weekly digest on Monday 08:00 UTC | + +--- + +## What This Roadmap Delivers + +| Today | After roadmap | +|---|---| +| `/api/vm/health` silently fails | VM module works; health data feeds dashboard | +| 8.2% steal is invisible | Daily alert + trend chart + score card dimension | +| "7 unhealthy" — no context, no fix | Drill-down to health log; auto-restart within 30 min | +| Cleanup log is a raw text dump | Structured panel: when, what, how much freed | +| invttrdg writing 22 GB/day — undetected | I/O alert + investigation complete | +| No memory guardrails on 39 containers | Per-container limits; OOM events alerted | +| 2 weeks of Prometheus data — no UI | Trend charts: disk projection, swap, steal over time | +| Manual VM diagnosis = 30 min SSH session | Score card auto-refreshes every 5 min | +| Ollama loads silently, may cause swap storm | RAM impact warning before load | + +--- + +## Change Log (v1 → v2) + +| # | What changed | Why | +|---|---|---| +| 1 | Added **Phase 0** (fix broken foundations) | devops-backend VM module non-functional; must fix first | +| 2 | Phase 4.1 changed from Cosmos DB → **Prometheus queries** | Prometheus already running with 2 weeks of history; Cosmos would be duplicate | +| 3 | Phase 2.1 restart explanation corrected | `unless-stopped` does not react to health check failures; process is alive | +| 4 | Phase 1.2 steal time corrected | Requires **2 samples** 1s apart, not single `/proc/stat` read | +| 5 | Phase 2.3 memory limits **validated against actual RSS data** | Prevents proposing limits that would OOM running services | +| 6 | Phase 5 added **invttrdg I/O investigation** + Grafana option | 22 GB/day block writes is the highest-risk untracked issue on the machine | +| 7 | Added Phase 0.2 **logrotate** for new log files | `/var/log/docker-watchdog.log` would grow unbounded | +| 8 | Added **architectural decisions** section (Prometheus vs Cosmos, host exec strategy) | Prevents wasted build on wrong approach | +| 9 | Added **success criteria** per phase | Makes "done" objective and testable | +| 10 | Added explicit **phase dependency map** | Phase 3 items would fail if built before Phase 0 | +| 11 | Corrected LLM status: `qwen2.5-coder:1.5b` **is currently loaded** | `ollama ps` confirmed; not idle as v1 stated | diff --git a/scripts/VMs/HostingerVM/vm-cleanup.sh b/scripts/VMs/HostingerVM/vm-cleanup.sh index b43eadf..4987038 100755 --- a/scripts/VMs/HostingerVM/vm-cleanup.sh +++ b/scripts/VMs/HostingerVM/vm-cleanup.sh @@ -183,6 +183,47 @@ do_uninstall_cron() { # ── Cleanup steps ───────────────────────────────────────────────────────────── +step_cosmos_pglog() { + # The Azure CosmosDB emulator uses an embedded Postgres instance that logs + # every SQL statement to /logs/pglog.log inside its overlay layer. + # It grows ~275 MB/hr during heavy trading activity. Truncate it safely — + # Postgres keeps the file descriptor open so truncation doesn't break it. + log_header "CosmosDB Emulator Postgres Log" + local container="learning_ai_common_plat-cosmos-emulator-1" + if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${container}$"; then + log_step SKIP "CosmosDB emulator not running" + return + fi + # Locate the overlay upper dir for this container + local pglog + pglog=$(docker inspect "$container" 2>/dev/null \ + | python3 -c " +import json,sys,os +d=json.load(sys.stdin)[0] +# Try direct GraphDriver path first +upper=d.get('GraphDriver',{}).get('Data',{}).get('UpperDir','') +if upper: + p=os.path.join(upper,'logs','pglog.log') + if os.path.exists(p): print(p) + exit() +# Fallback: scan rootfs overlayfs dirs +import glob +for f in glob.glob('/var/lib/docker/rootfs/overlayfs/*/logs/pglog.log'): + print(f); exit() +" 2>/dev/null || true) + if [[ -z "$pglog" || ! -f "$pglog" ]]; then + log_step SKIP "pglog.log not found (overlay path changed?)" + return + fi + local size_mb + size_mb=$(du -sm "$pglog" 2>/dev/null | cut -f1 || echo 0) + if (( size_mb < 100 )); then + log_step SKIP "pglog.log is ${size_mb}MB — no truncation needed (<100 MB)" + return + fi + run_cmd SAFE "Truncate CosmosDB pglog.log (${size_mb}MB → 0)" truncate -s 0 "$pglog" +} + step_docker_build_cache() { log_header "Docker Build Cache" if ! docker info &>/dev/null 2>&1; then @@ -207,6 +248,21 @@ step_docker_system_prune() { docker system prune -f } +step_docker_aged_images() { + # Removes tagged images that haven't been used by any container in >7 days. + # Safe because any running container holds a reference to its image — this + # only cleans up old image versions that were replaced (e.g. after a deploy). + log_header "Docker Aged Image Prune (unused >7 days)" + if ! docker info &>/dev/null 2>&1; then + log_step SKIP "Docker not running" + return + fi + local reclaimable + reclaimable=$(docker system df 2>/dev/null | awk '/^Images/ {print $4}' || echo "?") + run_cmd SAFE "Prune images unused for >7 days (currently $reclaimable reclaimable)" \ + docker image prune -a -f --filter "until=168h" +} + step_docker_crash_loop_check() { log_header "Crash Loop Check" if ! docker info &>/dev/null 2>&1; then return; fi @@ -260,7 +316,7 @@ step_next_cache() { while IFS= read -r cache_dir; do log_step SAFE "Remove $cache_dir" if ! $DRY_RUN; then rm -rf "$cache_dir"; fi - (( count++ )) + count=$(( count + 1 )) done < <( find /opt/bytelyst -name ".next" -maxdepth 7 -type d 2>/dev/null \ | while read -r d; do @@ -287,7 +343,7 @@ step_old_logs() { for f in /var/log/syslog.1 /var/log/kern.log.1 /var/log/ufw.log.1; do if [[ -f "$f" && ! -f "${f}.gz" ]]; then run_cmd SAFE "Compress $f" gzip -9 "$f" - (( count++ )) + count=$(( count + 1 )) fi done # Remove log rotations older than 30 days @@ -307,7 +363,7 @@ step_hold_cleanup() { size=$(du -sm "$nm" 2>/dev/null | cut -f1 || echo "0") run_cmd CAREFUL "Delete archived node_modules: $nm (~${size}MB)" rm -rf "$nm" total_freed=$(( total_freed + size )) - (( found++ )) + found=$(( found + 1 )) done < <( find /opt/bytelyst/HOLD -name "node_modules" -maxdepth 4 -type d 2>/dev/null || true ) @@ -345,6 +401,7 @@ log "[START] mode=${_mode} dry=$DRY_RUN" record_disk_before # ── WEEKLY (always run) ────────────────────────────────────────────────────── +step_cosmos_pglog step_docker_build_cache step_docker_crash_loop_check step_journal @@ -355,6 +412,7 @@ step_next_cache # ── MONTHLY (only with --full) ─────────────────────────────────────────────── if $FULL_MODE; then step_docker_system_prune + step_docker_aged_images step_pnpm_store step_old_logs step_hold_cleanup