From be46a81b8ff702f9820ba3374573f40ec6eb133d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 1 Nov 2024 19:15:12 +0100 Subject: [PATCH 001/369] Refactor - Breaking Changes for current DockStat frontend (#18) * Advanced logging * remove docker.io/library from images * updated entrypoint for cup integration * added swagger and more * updated new routes and added first routes which will be controlled by the frontend * Added auth (still buggy here and there) * fixed auth problem when trying to use swagger * Add more logging * added databse functionality * test for auto commit message * test auto git changelog * Update changelog.yml * Update changelog.yml * test new CHANGELOG.md * Changed Files: -------------- .github/workflows/changelog.yml CHANGELOG.md data/database.db package-lock.json package.json * Added offline development capabilities * Advanced frontend customization and new dependecy graph generator using mermaid diagrams and dependecy-cruiser * Dependency cruiser and gitignore adjustmnts * better port assignment, when running in a non-docker environment * Adjust workflows and adjusted entrypoint.sh file * Rate limiter * Rate limiter * adjust workflows --------- Co-authored-by: ItsNik Co-authored-by: root --- .dependency-cruiser.js | 380 +++ .github/workflows/build-dev.yaml | 43 +- .github/workflows/build-image.yml | 44 +- .gitignore | 147 +- Dockerfile | 66 +- LICENSE | 56 +- README.md | 87 +- config/apprise_config_example.yml | 5 - config/db.js | 19 + config/dockerConfig.json | 9 + config/hosts.yaml | 24 - config/loggerConfig.js | 16 + config/swaggerConfig.js | 28 + controllers/containerController.js | 49 + controllers/fetchData.js | 34 + controllers/frontendConfiguration.js | 180 ++ controllers/scheduler.js | 75 + data/database.db | Bin 0 -> 16384 bytes dockstatapi.js | 379 --- entrypoint.sh | 27 +- logger.js | 24 - middleware/authMiddleware.js | 50 + middleware/password.json | 1 + middleware/usePassword.txt | 1 + misc/dependencyGraphs/mermaid-all.txt | 70 + misc/dependencyGraphs/mermaid-api.txt | 33 + misc/dependencyGraphs/mermaid-auth.txt | 8 + misc/dependencyGraphs/mermaid-conf.txt | 26 + misc/dependencyGraphs/mermaid-data.txt | 11 + misc/dependencyGraphs/mermaid-frontend.txt | 11 + misc/entrypoint.sh | 26 + modules/updateAvailable.js | 32 - package-lock.json | 2507 +++++++++++++++++++- package.json | 24 +- routes/auth/routes.js | 145 ++ routes/data/routes.js | 111 + routes/frontendController/routes.js | 340 +++ routes/getter/routes.js | 334 +++ routes/setter/routes.js | 145 ++ scripts/install_apprise.sh | 16 - scripts/notify.sh | 47 - server.js | 33 + swagger/swaggerDocs.js | 10 + utils/containerService.js | 63 + utils/createDependencyGraph.sh | 34 + utils/dockerClient.js | 45 + utils/extractHostData.js | 26 + utils/logger.js | 20 + utils/rateLimiter.js | 8 + utils/writeOfflineLog.js | 31 + 50 files changed, 5150 insertions(+), 750 deletions(-) create mode 100644 .dependency-cruiser.js delete mode 100644 config/apprise_config_example.yml create mode 100644 config/db.js create mode 100644 config/dockerConfig.json delete mode 100644 config/hosts.yaml create mode 100644 config/loggerConfig.js create mode 100644 config/swaggerConfig.js create mode 100644 controllers/containerController.js create mode 100644 controllers/fetchData.js create mode 100644 controllers/frontendConfiguration.js create mode 100644 controllers/scheduler.js create mode 100644 data/database.db delete mode 100644 dockstatapi.js delete mode 100644 logger.js create mode 100644 middleware/authMiddleware.js create mode 100644 middleware/password.json create mode 100644 middleware/usePassword.txt create mode 100644 misc/dependencyGraphs/mermaid-all.txt create mode 100644 misc/dependencyGraphs/mermaid-api.txt create mode 100644 misc/dependencyGraphs/mermaid-auth.txt create mode 100644 misc/dependencyGraphs/mermaid-conf.txt create mode 100644 misc/dependencyGraphs/mermaid-data.txt create mode 100644 misc/dependencyGraphs/mermaid-frontend.txt create mode 100644 misc/entrypoint.sh delete mode 100644 modules/updateAvailable.js create mode 100644 routes/auth/routes.js create mode 100644 routes/data/routes.js create mode 100644 routes/frontendController/routes.js create mode 100644 routes/getter/routes.js create mode 100644 routes/setter/routes.js delete mode 100644 scripts/install_apprise.sh delete mode 100755 scripts/notify.sh create mode 100644 server.js create mode 100644 swagger/swaggerDocs.js create mode 100644 utils/containerService.js create mode 100755 utils/createDependencyGraph.sh create mode 100644 utils/dockerClient.js create mode 100644 utils/extractHostData.js create mode 100644 utils/logger.js create mode 100644 utils/rateLimiter.js create mode 100644 utils/writeOfflineLog.js diff --git a/.dependency-cruiser.js b/.dependency-cruiser.js new file mode 100644 index 00000000..07df12bf --- /dev/null +++ b/.dependency-cruiser.js @@ -0,0 +1,380 @@ +/** @type {import('dependency-cruiser').IConfiguration} */ +module.exports = { + forbidden: [ + { + name: 'no-circular', + severity: 'warn', + comment: + 'This dependency is part of a circular relationship. You might want to revise ' + + 'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ', + from: {}, + to: { + circular: true + } + }, + { + name: 'no-orphans', + comment: + "This is an orphan module - it's likely not used (anymore?). Either use it or " + + "remove it. If it's logical this module is an orphan (i.e. it's a config file), " + + "add an exception for it in your dependency-cruiser configuration. By default " + + "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + + "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", + severity: 'warn', + from: { + orphan: true, + pathNot: [ + '(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$', // dot files + '[.]d[.]ts$', // TypeScript declaration files + '(^|/)tsconfig[.]json$', // TypeScript config + '(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$' // other configs + ] + }, + to: {}, + }, + { + name: 'no-deprecated-core', + comment: + 'A module depends on a node core module that has been deprecated. Find an alternative - these are ' + + "bound to exist - node doesn't deprecate lightly.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'core' + ], + path: [ + '^v8/tools/codemap$', + '^v8/tools/consarray$', + '^v8/tools/csvparser$', + '^v8/tools/logreader$', + '^v8/tools/profile_view$', + '^v8/tools/profile$', + '^v8/tools/SourceMap$', + '^v8/tools/splaytree$', + '^v8/tools/tickprocessor-driver$', + '^v8/tools/tickprocessor$', + '^node-inspect/lib/_inspect$', + '^node-inspect/lib/internal/inspect_client$', + '^node-inspect/lib/internal/inspect_repl$', + '^async_hooks$', + '^punycode$', + '^domain$', + '^constants$', + '^sys$', + '^_linklist$', + '^_stream_wrap$' + ], + } + }, + { + name: 'not-to-deprecated', + comment: + 'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' + + 'version of that module, or find an alternative. Deprecated modules are a security risk.', + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'deprecated' + ] + } + }, + { + name: 'no-non-package-json', + severity: 'error', + comment: + "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + + "That's problematic as the package either (1) won't be available on live (2 - worse) will be " + + "available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " + + "in your package.json.", + from: {}, + to: { + dependencyTypes: [ + 'npm-no-pkg', + 'npm-unknown' + ] + } + }, + { + name: 'not-to-unresolvable', + comment: + "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + + 'module: add it to your package.json. In all other cases you likely already know what to do.', + severity: 'error', + from: {}, + to: { + couldNotResolve: true + } + }, + { + name: 'no-duplicate-dep-types', + comment: + "Likely this module depends on an external ('npm') package that occurs more than once " + + "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + + "maintenance problems later on.", + severity: 'warn', + from: {}, + to: { + moreThanOneDependencyType: true, + // as it's pretty common to have a type import be a type only import + // _and_ (e.g.) a devDependency - don't consider type-only dependency + // types for this rule + dependencyTypesNot: ["type-only"] + } + }, + + /* rules you might want to tweak for your specific situation: */ + + { + name: 'not-to-spec', + comment: + 'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' + + "If there's something in a spec that's of use to other modules, it doesn't have that single " + + 'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.', + severity: 'error', + from: {}, + to: { + path: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' + } + }, + { + name: 'not-to-dev-dep', + severity: 'error', + comment: + "This module depends on an npm package from the 'devDependencies' section of your " + + 'package.json. It looks like something that ships to production, though. To prevent problems ' + + "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + + 'section of your package.json. If this module is development only - add it to the ' + + 'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration', + from: { + path: '^(\./)', + pathNot: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' + }, + to: { + dependencyTypes: [ + 'npm-dev', + ], + // type only dependencies are not a problem as they don't end up in the + // production code or are ignored by the runtime. + dependencyTypesNot: [ + 'type-only' + ], + pathNot: [ + 'node_modules/@types/' + ] + } + }, + { + name: 'optional-deps-used', + severity: 'info', + comment: + "This module depends on an npm package that is declared as an optional dependency " + + "in your package.json. As this makes sense in limited situations only, it's flagged here. " + + "If you're using an optional dependency here by design - add an exception to your" + + "dependency-cruiser configuration.", + from: {}, + to: { + dependencyTypes: [ + 'npm-optional' + ] + } + }, + { + name: 'peer-deps-used', + comment: + "This module depends on an npm package that is declared as a peer dependency " + + "in your package.json. This makes sense if your package is e.g. a plugin, but in " + + "other cases - maybe not so much. If the use of a peer dependency is intentional " + + "add an exception to your dependency-cruiser configuration.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'npm-peer' + ] + } + } + ], + options: { + + /* Which modules not to follow further when encountered */ + doNotFollow: { + /* path: an array of regular expressions in strings to match against */ + path: ['node_modules'] + }, + + /* Which modules to exclude */ + // exclude : { + // /* path: an array of regular expressions in strings to match against */ + // path: '', + // }, + + /* Which modules to exclusively include (array of regular expressions in strings) + dependency-cruiser will skip everything not matching this pattern + */ + // includeOnly : [''], + + /* List of module systems to cruise. + When left out dependency-cruiser will fall back to the list of _all_ + module systems it knows of. It's the default because it's the safe option + It might come at a performance penalty, though. + moduleSystems: ['amd', 'cjs', 'es6', 'tsd'] + + As in practice only commonjs ('cjs') and ecmascript modules ('es6') + are widely used, you can limit the moduleSystems to those. + */ + + // moduleSystems: ['cjs', 'es6'], + + /* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/main/' + to open it on your online repo or `vscode://file/${process.cwd()}/` to + open it in visual studio code), + */ + // prefix: `vscode://file/${process.cwd()}/`, + + /* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation + true: also detect dependencies that only exist before typescript-to-javascript compilation + "specify": for each dependency identify whether it only exists before compilation or also after + */ + // tsPreCompilationDeps: false, + + /* list of extensions to scan that aren't javascript or compile-to-javascript. + Empty by default. Only put extensions in here that you want to take into + account that are _not_ parsable. + */ + // extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"], + + /* if true combines the package.jsons found from the module up to the base + folder the cruise is initiated from. Useful for how (some) mono-repos + manage dependencies & dependency definitions. + */ + // combinedDependencies: false, + + /* if true leave symlinks untouched, otherwise use the realpath */ + // preserveSymlinks: false, + + /* TypeScript project file ('tsconfig.json') to use for + (1) compilation and + (2) resolution (e.g. with the paths property) + + The (optional) fileName attribute specifies which file to take (relative to + dependency-cruiser's current working directory). When not provided + defaults to './tsconfig.json'. + */ + // tsConfig: { + // fileName: 'tsconfig.json' + // }, + + /* Webpack configuration to use to get resolve options from. + + The (optional) fileName attribute specifies which file to take (relative + to dependency-cruiser's current working directory. When not provided defaults + to './webpack.conf.js'. + + The (optional) `env` and `arguments` attributes contain the parameters + to be passed if your webpack config is a function and takes them (see + webpack documentation for details) + */ + // webpackConfig: { + // fileName: 'webpack.config.js', + // env: {}, + // arguments: {} + // }, + + /* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use + for compilation + */ + // babelConfig: { + // fileName: '.babelrc', + // }, + + /* List of strings you have in use in addition to cjs/ es6 requires + & imports to declare module dependencies. Use this e.g. if you've + re-declared require, use a require-wrapper or use window.require as + a hack. + */ + // exoticRequireStrings: [], + + /* options to pass on to enhanced-resolve, the package dependency-cruiser + uses to resolve module references to disk. The values below should be + suitable for most situations + + If you use webpack: you can also set these in webpack.conf.js. The set + there will override the ones specified here. + */ + enhancedResolveOptions: { + /* What to consider as an 'exports' field in package.jsons */ + exportsFields: ["exports"], + /* List of conditions to check for in the exports field. + Only works when the 'exportsFields' array is non-empty. + */ + conditionNames: ["import", "require", "node", "default", "types"], + /* + The extensions, by default are the same as the ones dependency-cruiser + can access (run `npx depcruise --info` to see which ones that are in + _your_ environment). If that list is larger than you need you can pass + the extensions you actually use (e.g. [".js", ".jsx"]). This can speed + up module resolution, which is the most expensive step. + */ + // extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"], + /* What to consider a 'main' field in package.json */ + + // if you migrate to ESM (or are in an ESM environment already) you will want to + // have "module" in the list of mainFields, like so: + // mainFields: ["module", "main", "types", "typings"], + mainFields: ["main", "types", "typings"], + /* + A list of alias fields in package.jsons + See [this specification](https://github.com/defunctzombie/package-browser-field-spec) and + the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields) + documentation + + Defaults to an empty array (= don't use alias fields). + */ + // aliasFields: ["browser"], + }, + reporterOptions: { + dot: { + /* pattern of modules that can be consolidated in the detailed + graphical dependency graph. The default pattern in this configuration + collapses everything in node_modules to one folder deep so you see + the external modules, but their innards. + */ + collapsePattern: 'node_modules/(?:@[^/]+/[^/]+|[^/]+)', + + /* Options to tweak the appearance of your graph.See + https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions + for details and some examples. If you don't specify a theme + dependency-cruiser falls back to a built-in one. + */ + // theme: { + // graph: { + // /* splines: "ortho" gives straight lines, but is slow on big graphs + // splines: "true" gives bezier curves (fast, not as nice as ortho) + // */ + // splines: "true" + // }, + // } + }, + archi: { + /* pattern of modules that can be consolidated in the high level + graphical dependency graph. If you use the high level graphical + dependency graph reporter (`archi`) you probably want to tweak + this collapsePattern to your situation. + */ + collapsePattern: '^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)', + + /* Options to tweak the appearance of your graph. If you don't specify a + theme for 'archi' dependency-cruiser will use the one specified in the + dot section above and otherwise use the default one. + */ + // theme: { }, + }, + "text": { + "highlightFocused": true + }, + } + } +}; +// generated: dependency-cruiser@16.5.0 on 2024-10-31T20:09:59.974Z diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index 72a370e7..a8d55f2c 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -2,7 +2,8 @@ name: Docker Image CI (dev) on: push: - branches: [ "dev" ] + branches: + - "dev" permissions: packages: write @@ -12,15 +13,35 @@ jobs: build-main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - name: Checkout repository + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - - uses: pmorelli92/github-container-registry-build-push@2.2.1 - name: Build and Publish latest service image + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ github.token }} + + - name: Generate Docker tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=sha,format=long,prefix= + flavor: | + type=schedule,pattern=nightly + + - name: Build and push + uses: docker/build-push-action@v5 with: - github-push-secret: ${{secrets.GITHUB_TOKEN}} - docker-image-name: dockstatapi - docker-image-tag: dev # optional - dockerfile-path: Dockerfile # optional - build-context: . # optional - build-only: false # optional + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 2836ac9a..8668f9ba 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -1,8 +1,8 @@ name: Docker Image CI on: - push: - branches: [ "main" ] + release: + types: [published] permissions: packages: write @@ -12,15 +12,35 @@ jobs: build-main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - name: Checkout repository + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - - uses: pmorelli92/github-container-registry-build-push@2.2.1 - name: Build and Publish latest service image + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ github.token }} + + - name: Generate Docker tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=sha,format=long,prefix= + flavor: | + latest=true + + - name: Build and push + uses: docker/build-push-action@v5 with: - github-push-secret: ${{secrets.GITHUB_TOKEN}} - docker-image-name: dockstatapi - docker-image-tag: latest # optional - dockerfile-path: Dockerfile # optional - build-context: . # optional - build-only: false # optional + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 2e7f14aa..f7fcc52b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,143 @@ -dockstat.log -node_modules -.dockerignore -apprise_config.yml \ No newline at end of file +# custom paths: +data/* + +# Created by https://www.toptal.com/developers/gitignore/api/node +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit diff --git a/Dockerfile b/Dockerfile index 8c70ae68..5fc294e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,32 @@ -# Stage 1: Build stage -FROM node:latest AS builder - -LABEL maintainer="https://github.com/its4nik" -LABEL version="1.0" -LABEL description="API for DockStat: Docker container statistics." -LABEL license="MIT" -LABEL repository="https://github.com/its4nik/dockstatapi" -LABEL documentation="https://github.com/its4nik/dockstatapi" - -WORKDIR /api - -COPY package*.json ./ - -RUN npm install --production - -COPY . . - -# Stage 2: Production stage -FROM node:alpine - -WORKDIR /api - -COPY --from=builder /api . - -RUN apk add --no-cache bash curl - -RUN bash /api/scripts/install_apprise.sh - -EXPOSE 7070 - -HEALTHCHECK CMD curl --fail http://localhost:7070/ || exit 1 - -ENTRYPOINT [ "bash", "entrypoint.sh" ] +# Stage 1: Build stage +FROM node:latest AS builder + +LABEL maintainer="https://github.com/its4nik" +LABEL version="2" +LABEL description="API for DockStat" +LABEL license="BSD-3-Clause license " +LABEL repository="https://github.com/its4nik/dockstatapi" +LABEL documentation="https://github.com/its4nik/dockstatapi" + +WORKDIR /api + +COPY package*.json ./ + +RUN npm install --production + +COPY . . + +# Stage 2: Production stage +FROM node:alpine + +WORKDIR /api + +COPY --from=builder /api . + +RUN apk add --no-cache bash curl + +EXPOSE 7070 + +HEALTHCHECK CMD curl --fail http://localhost:7070/api/status || exit 1 + +ENTRYPOINT [ "bash", "misc/entrypoint.sh" ] diff --git a/LICENSE b/LICENSE index 0a731244..1e9ecebd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,28 +1,28 @@ -BSD 3-Clause License - -Copyright (c) 2024, ItsNik - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +BSD 3-Clause License + +Copyright (c) 2024, ItsNik + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 0735e1af..c12afae4 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,13 @@ -# DockstatAPI +# DockStatAPI v2 -## This tool relies on the [DockerSocket Proxy](https://docs.linuxserver.io/images/docker-socket-proxy/), please see it's documentation for more information. +This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. +With this new release a cupple of extra features (compared to v1) are going to be available. -This is the DockStatAPI used in [DockStat](https://github.com/its4nik/dockstat). +### Feature List: -It features an easy way to configure using a yaml file. +- Swagger API Documentation +- "Offline" mode (useful when working on the backend without available test docker sockets) +- Database (Keeps data for 24 hours max) +- Advanced authentication using hashes and salt -You can specify multiple hosts, using a Docker Socket Proxy like this: - -## Installation: - -docker-compose.yaml -```yaml -services: - dockstatapi: - image: ghcr.io/its4nik/dockstatapi:latest - container_name: dockstatapi - environment: - - SECRET=CHANGEME # This is required in the header 'Authorization': 'CHANGEME' - - ALLOW_LOCALHOST="False" # Defaults to False - ports: - - "7070:7070" - volumes: - - ./dockstatapi:/api/config # Place your hosts.yaml file here - restart: always -``` - -Example docker-socket onfiguration: - -```yaml -socket-proxy: - image: lscr.io/linuxserver/socket-proxy:latest - container_name: socket-proxy - environment: - - CONTAINERS=1 # Needed for the api to worrk - - INFO=1 # Needed for the api to work - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - restart: unless-stopped - read_only: true - tmpfs: - - /run - ports: - - 2375:2375 -``` - -Configuration inside the mounted folder, as hosts.yaml -```yaml -mintimeout: 10000 # The minimum time to wait before querying the same server again, defaults to 5000 Ms - -log: - logsize: 10 # Specify the Size of the log files in MB, default is 1MB - LogCount: 1 # How many log files should be kept in rotation. Default is 5 - -hosts: - YourHost1: - url: hetzner - port: 2375 - -# This is used for DockStat -# Please see the dockstat documentation for more information -tags: - raspberry: red-200 - private: violet-400 - -container: - dozzle: # Container name - link: https://github.com -``` - -Please see the documentation for more information on what endpoints will be provieded. - -[Documentation](https://outline.itsnik.de/s/dockstat/doc/backend-api-reference-YzcBbDvY33) - ---- - -This Api uses a "queuing" mechanism to communicate to the servers, so that we dont ask the same server multiple times without getting an answer. - -Feel free to use this API in any of your projects :D - -The `/stats` endpoint server all information that are gethered from the server in a json format. +# 🔗 DockStatAPI v2 Documentation diff --git a/config/apprise_config_example.yml b/config/apprise_config_example.yml deleted file mode 100644 index 88e33870..00000000 --- a/config/apprise_config_example.yml +++ /dev/null @@ -1,5 +0,0 @@ -# Please see the apprise documentation -urls: - - tgram://bottoken/ChatID - - rocket://user:password@hostname/RoomID/Channel - - ntfy://topic/ diff --git a/config/db.js b/config/db.js new file mode 100644 index 00000000..9317ab40 --- /dev/null +++ b/config/db.js @@ -0,0 +1,19 @@ +const sqlite3 = require('sqlite3').verbose(); +const logger = require('./../utils/logger'); +const path = require('path'); +const dbPath = path.join(__dirname, '../data/database.db'); + +const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + logger.error('Error opening database:', err.message); + } else { + db.run(`CREATE TABLE IF NOT EXISTS data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + info TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + )`); + logger.info('Database created / opened succesfully'); + } +}); + +module.exports = db; \ No newline at end of file diff --git a/config/dockerConfig.json b/config/dockerConfig.json new file mode 100644 index 00000000..9ec4caf3 --- /dev/null +++ b/config/dockerConfig.json @@ -0,0 +1,9 @@ +{ + "hosts": [ + { + "name": "Fin-2", + "url": "100.89.35.135", + "port": "2375" + } + ] +} diff --git a/config/hosts.yaml b/config/hosts.yaml deleted file mode 100644 index d40e6697..00000000 --- a/config/hosts.yaml +++ /dev/null @@ -1,24 +0,0 @@ -mintimeout: 10000 # The minimum time to wait before querying the same server again, defaults to 5000 Ms - -log: - logsize: 10 # Specify the Size of the log files in MB, default is 1MB - LogCount: 1 # How many log files should be kept in rotation. Default is 5 - -tags: - raspberry: red-200 - private: violet-400 - -hosts: - YourHost1: - url: hetzner - port: 2375 - - YourHost2: - url: 100.78.180.21 - port: 2375 - -container: - dozzle: # Container name - link: https://github.com - icon: minecraft.png - tags: private,raspberry diff --git a/config/loggerConfig.js b/config/loggerConfig.js new file mode 100644 index 00000000..79503488 --- /dev/null +++ b/config/loggerConfig.js @@ -0,0 +1,16 @@ +const { format } = require('winston'); + +module.exports = { + level: 'info', + format: format.combine( + format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + format.printf(({ timestamp, level, message }) => `${timestamp} [${level.toUpperCase()}]: ${message}`) + ), + transports: { + console: true, + file: { + enabled: true, + filename: 'logs/app.log', + }, + }, +}; diff --git a/config/swaggerConfig.js b/config/swaggerConfig.js new file mode 100644 index 00000000..c79ae476 --- /dev/null +++ b/config/swaggerConfig.js @@ -0,0 +1,28 @@ +const options = { + definition: { + openapi: '3.0.0', + info: { + title: 'Your API Documentation', + version: '1.0.0', + description: 'API documentation with authentication', + }, + components: { + securitySchemes: { + passwordAuth: { + type: 'apiKey', + in: 'header', + name: 'x-password', + description: 'Password required for authentication', + }, + }, + }, + security: [ + { + passwordAuth: [], + }, + ], + }, + apis: ['./routes/*/*.js'], // Point to your route files +}; + +module.exports = options; diff --git a/controllers/containerController.js b/controllers/containerController.js new file mode 100644 index 00000000..f62ec5ce --- /dev/null +++ b/controllers/containerController.js @@ -0,0 +1,49 @@ +const fs = require("fs"); +const path = require("path"); +const { getDockerClient } = require("../utils/dockerClient"); +const logger = require("../utils/logger"); + +const getContainers = async (req, res) => { + const host = req.query.host || "local"; + logger.info(`Fetching containers from host: ${host}`); + try { + const docker = getDockerClient(host); + const containers = await docker.listContainers(); + + res.status(200).json(containers); + } catch (err) { + logger.error( + `Error fetching containers from host: ${host} - ${err.message || "Unknown error"} - Full error: ${JSON.stringify(err, null, 2)}`, + ); + res.status(500).json({ + error: `Error fetching containers: ${err.message || "Unknown error"}`, + }); + } +}; + +const getContainerStats = async (containerID, containerHost) => { + logger.info( + `Fetching stats for container: ${containerID} from host: ${containerHost}`, + ); + try { + const docker = getDockerClient(containerHost); + const container = docker.getContainer(containerID); + const stats = await container.stats({ stream: false }); + logger.info( + `Successfully fetched stats for container: ${containerID} from host: ${containerHost}`, + ); + res.status(200).json(stats); + } catch (error) { + logger.error( + `Error fetching stats for container: ${containerID} from host: ${containerHost} - ${error.message}`, + ); + res + .status(500) + .json({ error: `Error fetching container stats: ${error.message}` }); + } +}; + +module.exports = { + getContainers, + getContainerStats, +}; diff --git a/controllers/fetchData.js b/controllers/fetchData.js new file mode 100644 index 00000000..43b4f8a1 --- /dev/null +++ b/controllers/fetchData.js @@ -0,0 +1,34 @@ +const db = require("../config/db"); +const { fetchAllContainers } = require("../utils/containerService"); +const logger = require("./../utils/logger"); +const path = require("path"); +const fs = require("fs"); + +const fetchData = async () => { + try { + const allContainerData = await fetchAllContainers(); + const data = allContainerData; + + if (process.env.OFFLINE === "true") { + logger.info("No new data inserted --- OFFLINE MODE"); + } else { + // Insert data into the SQLite database + db.run( + `INSERT INTO data (info) VALUES (?)`, + [JSON.stringify(data)], + function (error) { + if (error) { + logger.info("Error inserting data:", error.message); + console.error("Error inserting data:", error.message); + return; + } + logger.info(`Data inserted with ID: ${this.lastID}`); + }, + ); + } + } catch (error) { + logger.error("Error fetching data:", error.message); + } +}; + +module.exports = fetchData; diff --git a/controllers/frontendConfiguration.js b/controllers/frontendConfiguration.js new file mode 100644 index 00000000..ff1ce3ea --- /dev/null +++ b/controllers/frontendConfiguration.js @@ -0,0 +1,180 @@ +const fs = require("fs"); +const path = require("path"); +const dataPath = path.join(__dirname, "../data/frontendConfiguration.json"); +const logger = require("../utils/logger"); + +async function hideContainer(containerName) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + data[containerIndex].hidden = true; + await saveData(data); + } else { + data.push({ name: containerName, hidden: true }); + await saveData(data); + } + } catch (error) { + logger.error(error); + } +} + +async function unhideContainer(containerName) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + delete data[containerIndex].hidden; + await saveData(data); + cleanupData(); + } + } catch (error) { + logger.error(error); + } +} + +async function addTagToContainer(containerName, tag) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + if (!data[containerIndex].tags) { + data[containerIndex].tags = []; + } + data[containerIndex].tags.push(tag); + await saveData(data); + } else { + data.push({ name: containerName, tags: [tag] }); + await saveData(data); + } + } catch (error) { + logger.error(error); + } +} + +async function removeTagFromContainer(containerName, tag) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1 && data[containerIndex].tags) { + data[containerIndex].tags = data[containerIndex].tags.filter( + (t) => t !== tag, + ); + await saveData(data); + cleanupData(); + } + } catch (error) { + logger.error(error); + } +} + +async function pinContainer(containerName) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + data[containerIndex].pinned = true; + await saveData(data); + } else { + data.push({ name: containerName, pinned: true }); + await saveData(data); + } + } catch (error) { + logger.error(error); + } +} + +async function unpinContainer(containerName) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + delete data[containerIndex].pinned; + await saveData(data); + cleanupData(); + } + } catch (error) { + logger.error(error); + } +} + +async function readData() { + try { + const data = await fs.promises.readFile(dataPath, "utf-8"); + return JSON.parse(data); + } catch (error) { + console.error("readData"); + if (error.code === "ENOENT") { + await saveData([]); + return []; + } else { + throw error; + } + } +} + +async function saveData(data) { + try { + await fs.promises.writeFile( + dataPath, + JSON.stringify(data, null, 2), + "utf-8", + ); + logger.info("Succesfully wrote to file"); + } catch (error) { + logger.error(error); + } +} + +async function cleanupData() { + try { + const data = await readData(); + let cleanedData = []; + + if (data && Array.isArray(data)) { + cleanedData = data.filter((container) => { + // Filter out containers with empty "tags" or containers with only one property (name) + if ( + container.tags && + Array.isArray(container.tags) && + container.tags.length === 0 + ) { + delete container.tags; + } + return Object.keys(container).length > 1; + }); + } + + await saveData(cleanedData); + } catch (error) { + logger.error(error); + } +} + +module.exports = { + hideContainer, + unhideContainer, + addTagToContainer, + removeTagFromContainer, + pinContainer, + unpinContainer, + cleanupData, +}; diff --git a/controllers/scheduler.js b/controllers/scheduler.js new file mode 100644 index 00000000..5bb3ca7b --- /dev/null +++ b/controllers/scheduler.js @@ -0,0 +1,75 @@ +const fetchData = require("./fetchData"); +const logger = require("../utils/logger"); +const db = require("../config/db"); + +let fetchInterval = 5 * 60 * 1000; +let intervalId; + +const scheduleFetch = () => { + fetchData().then(() => { + cleanupOldEntries(); + }); + intervalId = setInterval(() => { + logger.info( + `Fetching data at interval of ${fetchInterval / 1000} seconds.`, + ); + cleanupOldEntries(); + fetchData(); + }, fetchInterval); + logger.info(`Data fetching scheduled every ${fetchInterval / 1000} seconds.`); +}; + +const setFetchInterval = (newInterval) => { + if (intervalId) { + clearInterval(intervalId); + logger.info(`Cleared existing fetch interval.`); + } + fetchInterval = newInterval; + scheduleFetch(); + logger.info(`Fetch interval updated to ${fetchInterval / 1000} seconds.`); +}; + +const parseInterval = (interval) => { + const timeUnits = { + s: 1000, + m: 60 * 1000, + h: 60 * 60 * 1000, + }; + + let totalMilliseconds = 0; + const regex = /(\d+)([smh])/g; + let match; + + while ((match = regex.exec(interval))) { + const value = parseInt(match[1], 10); + const unit = match[2]; + totalMilliseconds += value * timeUnits[unit]; + } + + return totalMilliseconds; +}; + +const getCurrentSchedule = () => { + return { + interval: fetchInterval / 1000, + }; +}; + +const cleanupOldEntries = async () => { + const twentyFourHoursAgo = new Date( + Date.now() - 24 * 60 * 60 * 1000, + ).toISOString(); + try { + await db.run("DELETE FROM data WHERE timestamp < ?", twentyFourHoursAgo); + logger.info(`Old entries cleared from the database.`); + } catch (error) { + logger.error(`Error clearing old entries: ${error.message}`); + } +}; + +module.exports = { + scheduleFetch, + setFetchInterval, + parseInterval, + getCurrentSchedule, +}; diff --git a/data/database.db b/data/database.db new file mode 100644 index 0000000000000000000000000000000000000000..80295980fc1caba1cff16d304197ec55893652f2 GIT binary patch literal 16384 zcmeI2-ELdQ6@?`ytyh63@AMkC zVd_0QK0W;1;Yshs$+M$_lUKdpAHM1xygdE<*)b*`9Uh-keRg!%`}FX$gO|@wd!M{KIl=YL>cKBg4~|~kUigFh-F)|P z+`(^m>j-oNIszSmjzCA?zenJ2zuEoS&fjFfSeVTs*5o8cM$jMvj?9?#C& z1DZz{12N#+$@J@^NxF;DS<1yL zWnLK1wUe5-6DU!{}kDB z;5xM13EQJvQq$7wz>3ulY+@|j7f)G=l5HR>WiUC~XqnTg=&Z>U%QEMQWkiK$VvB%l zQ&XBdC(YvEJ=6pv=V(O@A$SL}<*0RJH>0=V_5Jr3c}Yuol{rgZNA3+pW${DSvqVNn zS3(ISlR+j*A}xdhP7{K&DPr zbI+QZ5*$pMv}!UpM>o&4XKhaN61C}kny;2TwNw-x2vRDjV*-h&3sZzj7m>448dNC7}v}=9huOzY;Iic48w;rkf>>nX=7L$sL~E;Z0!x@d%_Xq72z{k1tdzc zcK9^zzbLD9pfZ%7DRs(BJ!_S|LQ6 zoN7QeMmJB@cx=piV}{4)!}04E)5+`aj{F#_;B*;~D1E%>kP?p;3yJj^tA))zdm*EX z9IrTQn_>`-nZhk&UMg$7%LM#6Y{63_2Am#XS;vZMEy)&6h2@J7>EgkFBx96eR+&bP z#MJ}|VNf8j#d&-F{r}y6?%=n(bp$#B9f6KON1!9n5$FhX1Udp8fsQ~&pd;}AMd0!7 zTTgyb+Y;*A`iZpC)D70Pm4<#$h5k)t`l|CGqQQSJ!5lOOR;~8fV&oth-g|P&*O>iuOVOiAR^h! zZQ)74`&1EfzsTEICWv^FoCYxCIN@QToi2jaFhr0x52w8lww4eEKWD`k;R=_}O1dNa zTNBptHYaXQci{&tqgJI%(S!)NE~2_=Pa$w0fiUX~56lpEsfxiim|}n)(sghy_>fHA z&WADpDcwN9MdFV5g5LrvtPl%9th z8BymGj~rdWbjlZnj2UFMu- z=-^x?bb*JYl|=AGq^wKDN&)9MA;X`@0Sz{wzMy(IcM`gTmm(Hl5B^pDVjf$a2dYj?wNydXS5)NcZ3jG+2MZ>nhs^&4t2DS5~ z#$=l+!E&G1z6V-F;Ct7}hBy3W^aa80MX{H-W|QDNLK_pB=W3X?CT@;*HoeYqG8*~u z?G_Zyd@81(gt3CHhU5vwU&8v$NtGlSiG>9DPLz)v-Bt}J+n1RKJZML9`inv&B zWc`E?XAirgycS$6z73EP4OZAYNo-e}s47Nk3~!#R7#q_zhjO$EC_Sn`q*`n^=qg1kqQ4FXWWc#hdfhZ2e!$_RZY=mv|}u2Mn(ZxNqujs}H< ztbUkdEfKQWw1g!^NbCgesFt^5*9kV}Y))`IE8i`-F;qkoYipcEcN&9TkClg`he`?N zGK&(cC~4*tJbR0F1i@5bpoXl*BE`2Umu+qwEfDxl@Gv)n78YabqKtPu|MqB5j+`=g g=J0atI=I!GH|6%_kAexc&-p&%J!YRWfv>Iq0`|MTqyPW_ literal 0 HcmV?d00001 diff --git a/dockstatapi.js b/dockstatapi.js deleted file mode 100644 index d06fb1e7..00000000 --- a/dockstatapi.js +++ /dev/null @@ -1,379 +0,0 @@ -const express = require('express'); -const path = require('path'); -const yaml = require('yamljs'); -const Docker = require('dockerode'); -const cors = require('cors'); -const fs = require('fs'); -const { exec } = require('child_process'); -const logger = require('./logger'); -const updateAvailable = require('./modules/updateAvailable') -const app = express(); -const port = 7070; -const key = process.env.SECRET || 'CHANGE-ME'; -const skipAuth = process.env.SKIP_AUTH || 'True' -const cupUrl = process.env.CUP_URL || 'null' - -let config = yaml.load('./config/hosts.yaml'); -let hosts = config.hosts; -let containerConfigs = config.container || {}; -let maxlogsize = config.log.logsize || 1; -let LogAmount = config.log.LogCount || 5; -let queryInterval = config.mintimeout || 5000; -let latestStats = {}; -let hostQueues = {}; -let previousNetworkStats = {}; -let generalStats = {}; -let previousContainerStates = {}; -let previousRunningContainers = {}; - - -app.use(cors()); -app.use(express.json()); - -const authenticateHeader = (req, res, next) => { - const authHeader = req.headers['authorization']; - - if (skipAuth === 'True') { - next(); - } else { - if (!authHeader || authHeader !== key) { - logger.error(`${authHeader} != ${key}`); - return res.status(401).json({ error: "Unauthorized" }); - } - else { - next(); - } - } -}; - -function createDockerClient(hostConfig) { - return new Docker({ - host: hostConfig.url, - port: hostConfig.port, - }); -} - -function getTagColor(tag) { - const tagsConfig = config.tags || {}; - return tagsConfig[tag] || ''; -} - -async function getContainerStats(docker, containerId) { - const container = docker.getContainer(containerId); - return new Promise((resolve, reject) => { - container.stats({ stream: false }, (err, stats) => { - if (err) return reject(err); - resolve(stats); - }); - }); -} - -async function handleContainerStateChanges(hostName, currentContainers) { - const currentRunningContainers = currentContainers - .filter(container => container.state === 'running') - .reduce((map, container) => { - map[container.id] = container; - return map; - }, {}); - - const previousHostContainers = previousRunningContainers[hostName] || {}; - - // Check for containers that have been removed or exited - for (const containerId of Object.keys(previousHostContainers)) { - const container = previousHostContainers[containerId]; - if (!currentRunningContainers[containerId]) { - if (container.state === 'running') { - // Container removed - exec(`bash ./scripts/notify.sh REMOVE ${containerId} ${container.name} ${hostName} ${container.state}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing REMOVE notify.sh: ${error.message}`); - } else { - logger.info(`Container removed: ${container.name} (${containerId}) from host ${hostName}`); - logger.info(stdout); - } - }); - } - else if (container.state === 'exited') { - // Container exited - exec(`bash ./scripts/notify.sh EXIT ${containerId} ${container.name} ${hostName} ${container.state}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing EXIT notify.sh: ${error.message}`); - } else { - logger.info(`Container exited: ${container.name} (${containerId}) from host ${hostName}`); - logger.info(stdout); - } - }); - } - } - } - - // Check for new containers or state changes - for (const containerId of Object.keys(currentRunningContainers)) { - const container = currentRunningContainers[containerId]; - const previousContainer = previousHostContainers[containerId]; - - if (!previousContainer) { - // New container added - exec(`bash ./scripts/notify.sh ADD ${containerId} ${container.name} ${hostName} ${container.state}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing ADD notify.sh: ${error.message}`); - } else { - logger.info(`Container added: ${container.name} (${containerId}) to host ${hostName}`); - logger.info(stdout); - } - }); - } else if (previousContainer.state !== container.state) { - // Container state has changed - const newState = container.state; - if (newState === 'exited') { - exec(`bash ./scripts/notify.sh EXIT ${containerId} ${container.name} ${hostName} ${newState}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing EXIT notify.sh: ${error.message}`); - } else { - logger.info(`Container exited: ${container.name} (${containerId}) from host ${hostName}`); - logger.info(stdout); - } - }); - } else { - // Any other state change - exec(`bash ./scripts/notify.sh ANY ${containerId} ${container.name} ${hostName} ${newState}`, (error, stdout, stderr) => { - if (error) { - logger.error(`Error executing ANY notify.sh: ${error.message}`); - } else { - logger.info(`Container state changed to ${newState}: ${container.name} (${containerId}) from host ${hostName}`); - logger.info(stdout); - } - }); - } - } - } - - // Update the previous state for the next comparison - previousRunningContainers[hostName] = currentRunningContainers; -} - -async function queryHostStats(hostName, hostConfig) { - logger.debug(`Querying Docker stats for host: ${hostName} (${hostConfig.url}:${hostConfig.port})`); - - const docker = createDockerClient(hostConfig); - - try { - const info = await docker.info(); - const totalMemory = info.MemTotal; - const totalCPUs = info.NCPU; - const containers = await docker.listContainers({ all: true }); - - const statsPromises = containers.map(async (container) => { - try { - const containerName = container.Names[0].replace('/', ''); - const containerState = container.State; - const updateAvailableFlag = await updateAvailable(container.Image, cupUrl); - let networkMode = container.HostConfig.NetworkMode; - - // Check if network mode is in the format "container:IDXXXXXXXX" - if (networkMode.startsWith("container:")) { - const linkedContainerId = networkMode.split(":")[1]; - const linkedContainer = await docker.getContainer(linkedContainerId).inspect(); - const linkedContainerName = linkedContainer.Name.replace('/', ''); // Remove leading slash - - networkMode = `Container: ${linkedContainerName}`; // Format the network mode - } - - if (containerState !== 'running') { - previousContainerStates[container.Id] = containerState; - return { - name: containerName, - id: container.Id, - hostName: hostName, - state: containerState, - image: container.Image, - update_available: updateAvailableFlag || false, - cpu_usage: 0, - mem_usage: 0, - mem_limit: 0, - net_rx: 0, - net_tx: 0, - current_net_rx: 0, - current_net_tx: 0, - networkMode: networkMode, - link: containerConfigs[containerName]?.link || '', - icon: containerConfigs[containerName]?.icon || '', - tags: getTagColor(containerConfigs[containerName]?.tags || ''), - }; - } - - // Fetch container stats for running containers - const containerStats = await getContainerStats(docker, container.Id); - const containerCpuUsage = containerStats.cpu_stats.cpu_usage.total_usage; - const containerMemoryUsage = containerStats.memory_stats.usage; - - let netRx = 0, netTx = 0, currentNetRx = 0, currentNetTx = 0; - - if (networkMode !== 'host' && containerStats.networks?.eth0) { - const previousStats = previousNetworkStats[container.Id] || { rx_bytes: 0, tx_bytes: 0 }; - currentNetRx = containerStats.networks.eth0.rx_bytes - previousStats.rx_bytes; - currentNetTx = containerStats.networks.eth0.tx_bytes - previousStats.tx_bytes; - - previousNetworkStats[container.Id] = { - rx_bytes: containerStats.networks.eth0.rx_bytes, - tx_bytes: containerStats.networks.eth0.tx_bytes, - }; - - netRx = containerStats.networks.eth0.rx_bytes; - netTx = containerStats.networks.eth0.tx_bytes; - } - - previousContainerStates[container.Id] = containerState; - const config = containerConfigs[containerName] || {}; - - const tagArray = (config.tags || '') - .split(',') - .map(tag => { - const color = getTagColor(tag); - return color ? `${tag}:${color}` : tag; - }) - .join(','); - - return { - name: containerName, - id: container.Id, - hostName: hostName, - image: container.Image, - update_available: updateAvailableFlag || false, - state: containerState, - cpu_usage: containerCpuUsage, - mem_usage: containerMemoryUsage, - mem_limit: containerStats.memory_stats.limit, - net_rx: netRx, - net_tx: netTx, - current_net_rx: currentNetRx, - current_net_tx: currentNetTx, - networkMode: networkMode, - link: config.link || '', - icon: config.icon || '', - tags: tagArray, - }; - } catch (err) { - logger.error(`Failed to fetch stats for container ${container.Names[0]} (${container.Id}): ${err.message}`); - return null; - } - }); - - const hostStats = await Promise.all(statsPromises); - const validStats = hostStats.filter(stat => stat !== null); - - const totalCpuUsage = validStats.reduce((acc, container) => acc + parseFloat(container.cpu_usage), 0); - const totalMemoryUsage = validStats.reduce((acc, container) => acc + container.mem_usage, 0); - const memoryUsagePercent = ((totalMemoryUsage / totalMemory) * 100).toFixed(2); - - generalStats[hostName] = { - containerCount: validStats.length, - totalCPUs: totalCPUs, - totalMemory: totalMemory, - cpuUsage: totalCpuUsage, - memoryUsage: memoryUsagePercent, - }; - - latestStats[hostName] = validStats; - - logger.debug(`Fetched stats for ${validStats.length} containers from ${hostName}`); - - // Handle container state changes - await handleContainerStateChanges(hostName, validStats); - } catch (err) { - logger.error(`Failed to fetch containers from ${hostName}: ${err.message}`); - } -} - - -async function handleHostQueue(hostName, hostConfig) { - while (true) { - await queryHostStats(hostName, hostConfig); - await new Promise(resolve => setTimeout(resolve, queryInterval)); - } -} - -// Initialize the host queues -function initializeHostQueues() { - for (const [hostName, hostConfig] of Object.entries(hosts)) { - hostQueues[hostName] = handleHostQueue(hostName, hostConfig); - } -} - -// Dynamically reloads the yaml file -function reloadConfig() { - for (const hostName in hostQueues) { - hostQueues[hostName] = null; - } - try { - config = yaml.load('./config/hosts.yaml'); - hosts = config.hosts; - containerConfigs = config.container || {}; - maxlogsize = config.log.logsize || 1; - LogAmount = config.log.LogCount || 5; - queryInterval = config.mintimeout || 5000; - - logger.info('Configuration reloaded successfully.'); - - initializeHostQueues(); - } catch (err) { - logger.error(`Failed to reload configuration: ${err.message}`); - } -} - -// Watch the YAML file for changes and reload the config -fs.watchFile('./config/hosts.yaml', (curr, prev) => { - if (curr.mtime !== prev.mtime) { - logger.info('Detected change in configuration file. Reloading...'); - reloadConfig(); - } -}); - -// Endpoint to get stats -app.get('/stats', authenticateHeader, (req, res) => { - res.json(latestStats); -}); - -// Endpoint for general Host based statistics -app.get('/hosts', authenticateHeader, (req, res) => { - res.json(generalStats); -}); - -// Read Only config endpoint -app.get('/config', authenticateHeader, (req, res) => { - const filePath = path.join(__dirname, './config/hosts.yaml'); - res.set('Content-Type', 'text/plain'); // Keep as plain text - fs.readFile(filePath, 'utf8', (err, data) => { - logger.debug('Requested config file: ' + filePath); - if (err) { - logger.error(err); - res.status(500).send('Error reading file'); - } else { - res.send(data); - } - }); -}); - -app.get('/', (req, res) => { - res.redirect(301, '/stats'); -}); - -app.get('/status', (req, res) => { - logger.info("Healthcheck requested"); - return res.status(200).send('UP'); -}); - -// Start the server and log the startup message -app.listen(port, () => { - logger.info('=============================== DockStat ===============================') - logger.info(`DockStatAPI is running on http://localhost:${port}/stats`); - logger.info(`Minimum timeout between stats queries is: ${queryInterval} milliseconds`); - logger.info(`The max size for Log files is: ${maxlogsize}MB`) - logger.info(`The amount of log files to keep is: ${LogAmount}`); - logger.info(`Secret Key: ${key}`) - logger.info(`Cup URL: ${cupUrl}`) - logger.info("Press Ctrl+C to stop the server."); - logger.info('========================================================================') -}); - -initializeHostQueues(); diff --git a/entrypoint.sh b/entrypoint.sh index df95b988..2008cdbd 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,7 +1,26 @@ -#!/usr/bin/env bash +#!/bin/bash -SECRET="${SECRET//\"}" +cat << EOF +Welcome to -export SECRET + ###### ###### #### ### ### #### ######### ###### ######### + ### ### ### ### ### ### ### ### ### ### ### ### + ### ### ### ### ### ###### #### ### ### ### ### + ### ### ### ### ### ### ### #### ### ############ ### + ### ### ### ### ### ### ### #### ### ### ### ### + ###### ###### #### ### ### #### ### ### ### ### (API) -exec npm run start \ No newline at end of file +Useful links: + +- Documentation: https://outline.itsnik.de/s/dockstat +- GitHub (Frontend): https://github.com/its4nik/dockstat +- GitHub (Backend): https://github.com/its4nik/dockstatapi +- API Documentation: http://localhost:7000/api-docs + +Summary: + +DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. + +EOF + +npm run start diff --git a/logger.js b/logger.js deleted file mode 100644 index ebaacc38..00000000 --- a/logger.js +++ /dev/null @@ -1,24 +0,0 @@ -const winston = require('winston'); -const yaml = require('yamljs'); -const config = yaml.load('./config/hosts.yaml'); - -const maxlogsize = config.log.logsize || 1; -const LogAmount = config.log.LogCount || 5; - -const logger = winston.createLogger({ - level: 'debug', - format: winston.format.combine( - winston.format.timestamp(), - winston.format.json() - ), - transports: [ - new winston.transports.Console(), - new winston.transports.File({ - filename: './logs/dockstat.log', - maxsize: 1024 * 1024 * maxlogsize, - maxFiles: LogAmount - }) - ] -}); - -module.exports = logger; \ No newline at end of file diff --git a/middleware/authMiddleware.js b/middleware/authMiddleware.js new file mode 100644 index 00000000..8ee6a688 --- /dev/null +++ b/middleware/authMiddleware.js @@ -0,0 +1,50 @@ +const bcrypt = require("bcrypt"); +const fs = require("fs"); +const path = require("path"); +const logger = require("../utils/logger"); +const passwordFile = path.join(__dirname, "password.json"); +const passwordBool = path.join(__dirname, "usePassword.txt"); + +function authMiddleware(req, res, next) { + fs.readFile(passwordBool, "utf8", (err, data) => { + if (err) { + logger.error("Error reading the file:", err); + return; + } + + const isAuthEnabled = data.trim() === "true"; + + if (!isAuthEnabled) { + return next(); + } + + const providedPassword = req.headers["x-password"]; + if (!providedPassword) { + logger.error("Password required - Denied"); + return res.status(401).json({ message: "Password required" }); + } + + fs.readFile(passwordFile, "utf8", (err, data) => { + if (err) { + logger.error("Error reading password"); + return res.status(500).json({ message: "Error reading password" }); + } + + const storedData = JSON.parse(data); + bcrypt.compare(providedPassword, storedData.hash, (err, result) => { + if (err) { + logger.error("Error validating password - Denied access"); + return res.status(500).json({ message: "Error validating password" }); + } + if (!result) { + console.error("Invalid Password - Denied access"); + return res.status(401).json({ message: "Invalid password" }); + } + + next(); + }); + }); + }); +} + +module.exports = authMiddleware; diff --git a/middleware/password.json b/middleware/password.json new file mode 100644 index 00000000..37a7c4c4 --- /dev/null +++ b/middleware/password.json @@ -0,0 +1 @@ +{"hash":"$2b$10$qGcNmciEGhX.PiB.ofHib.Fob.nOjQNfguBoD4JDbbbTysrLrKGEi","salt":"$2b$10$qGcNmciEGhX.PiB.ofHib."} \ No newline at end of file diff --git a/middleware/usePassword.txt b/middleware/usePassword.txt new file mode 100644 index 00000000..02e4a84d --- /dev/null +++ b/middleware/usePassword.txt @@ -0,0 +1 @@ +false \ No newline at end of file diff --git a/misc/dependencyGraphs/mermaid-all.txt b/misc/dependencyGraphs/mermaid-all.txt new file mode 100644 index 00000000..87ba7da1 --- /dev/null +++ b/misc/dependencyGraphs/mermaid-all.txt @@ -0,0 +1,70 @@ +flowchart LR + +subgraph 0["config"] +1["db.js"] +2["swaggerConfig.js"] +9["dockerConfig.json"] +end +subgraph 3["controllers"] +4["containerController.js"] +7["fetchData.js"] +A["frontendConfiguration.js"] +B["scheduler.js"] +end +subgraph 5["utils"] +6["dockerClient.js"] +8["containerService.js"] +N["extractHostData.js"] +O["writeOfflineLog.js"] +U["rateLimiter.js"] +end +subgraph C["middleware"] +D["authMiddleware.js"] +end +subgraph E["routes"] +subgraph F["auth"] +G["routes.js"] +end +subgraph H["data"] +I["routes.js"] +end +subgraph J["frontendController"] +K["routes.js"] +end +subgraph L["getter"] +M["routes.js"] +end +subgraph P["setter"] +Q["routes.js"] +end +end +R["server.js"] +subgraph S["swagger"] +T["swaggerDocs.js"] +end +4-->6 +7-->1 +7-->8 +8-->9 +8-->6 +B-->1 +B-->7 +I-->1 +K-->A +M-->9 +M-->B +M-->8 +M-->6 +M-->N +M-->O +Q-->B +R-->B +R-->D +R-->G +R-->I +R-->K +R-->M +R-->Q +R-->T +R-->U +T-->2 diff --git a/misc/dependencyGraphs/mermaid-api.txt b/misc/dependencyGraphs/mermaid-api.txt new file mode 100644 index 00000000..c2dd6c86 --- /dev/null +++ b/misc/dependencyGraphs/mermaid-api.txt @@ -0,0 +1,33 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["getter"] +2["routes.js"] +end +end +subgraph 3["config"] +4["dockerConfig.json"] +7["db.js"] +end +subgraph 5["controllers"] +6["scheduler.js"] +8["fetchData.js"] +end +subgraph 9["utils"] +A["containerService.js"] +B["dockerClient.js"] +C["extractHostData.js"] +D["writeOfflineLog.js"] +end +2-->4 +2-->6 +2-->A +2-->B +2-->C +2-->D +6-->7 +6-->8 +8-->7 +8-->A +A-->4 +A-->B diff --git a/misc/dependencyGraphs/mermaid-auth.txt b/misc/dependencyGraphs/mermaid-auth.txt new file mode 100644 index 00000000..e7ab0669 --- /dev/null +++ b/misc/dependencyGraphs/mermaid-auth.txt @@ -0,0 +1,8 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["auth"] +2["routes.js"] +end +end + diff --git a/misc/dependencyGraphs/mermaid-conf.txt b/misc/dependencyGraphs/mermaid-conf.txt new file mode 100644 index 00000000..65e4b74a --- /dev/null +++ b/misc/dependencyGraphs/mermaid-conf.txt @@ -0,0 +1,26 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["setter"] +2["routes.js"] +end +end +subgraph 3["controllers"] +4["scheduler.js"] +7["fetchData.js"] +end +subgraph 5["config"] +6["db.js"] +A["dockerConfig.json"] +end +subgraph 8["utils"] +9["containerService.js"] +B["dockerClient.js"] +end +2-->4 +4-->6 +4-->7 +7-->6 +7-->9 +9-->A +9-->B diff --git a/misc/dependencyGraphs/mermaid-data.txt b/misc/dependencyGraphs/mermaid-data.txt new file mode 100644 index 00000000..e212edcb --- /dev/null +++ b/misc/dependencyGraphs/mermaid-data.txt @@ -0,0 +1,11 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["data"] +2["routes.js"] +end +end +subgraph 3["config"] +4["db.js"] +end +2-->4 diff --git a/misc/dependencyGraphs/mermaid-frontend.txt b/misc/dependencyGraphs/mermaid-frontend.txt new file mode 100644 index 00000000..35b4e61b --- /dev/null +++ b/misc/dependencyGraphs/mermaid-frontend.txt @@ -0,0 +1,11 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["frontendController"] +2["routes.js"] +end +end +subgraph 3["controllers"] +4["frontendConfiguration.js"] +end +2-->4 diff --git a/misc/entrypoint.sh b/misc/entrypoint.sh new file mode 100644 index 00000000..2008cdbd --- /dev/null +++ b/misc/entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +cat << EOF +Welcome to + + ###### ###### #### ### ### #### ######### ###### ######### + ### ### ### ### ### ### ### ### ### ### ### ### + ### ### ### ### ### ###### #### ### ### ### ### + ### ### ### ### ### ### ### #### ### ############ ### + ### ### ### ### ### ### ### #### ### ### ### ### + ###### ###### #### ### ### #### ### ### ### ### (API) + +Useful links: + +- Documentation: https://outline.itsnik.de/s/dockstat +- GitHub (Frontend): https://github.com/its4nik/dockstat +- GitHub (Backend): https://github.com/its4nik/dockstatapi +- API Documentation: http://localhost:7000/api-docs + +Summary: + +DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. + +EOF + +npm run start diff --git a/modules/updateAvailable.js b/modules/updateAvailable.js deleted file mode 100644 index 1a25ce3e..00000000 --- a/modules/updateAvailable.js +++ /dev/null @@ -1,32 +0,0 @@ -const logger = require('../logger'); - -async function getData(target, url) { - - if (url === 'null') { - return false; - } - else { - try { - const response = await fetch(`${url}/json`, { - method: "GET" - }); - if (!response.ok) { - throw new Error(`Response status: ${response.status}`); - } - - const json = await response.json(); - - const images = json.images; - - for (const image in images) { - if (target === image) { - return images.hasOwnProperty(target); - } - } - } catch (error) { - logger.error(error.message); - } - } -} - -module.exports = getData; diff --git a/package-lock.json b/package-lock.json index 37c8cf27..68d93740 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,67 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcrypt": "^5.1.1", "child_process": "^1.0.2", "cors": "^2.8.5", "dockerode": "^4.0.2", - "express": "^4.21.0", + "express": "^4.21.1", + "express-rate-limit": "^7.4.1", "node-fetch": "^3.3.2", - "winston": "^3.14.2", + "python-shell": "^5.0.0", + "sqlite3": "^5.1.7", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "winston": "^3.15.0", "yamljs": "^0.3.0" + }, + "devDependencies": { + "dependency-cruiser": "^16.5.0", + "nodemon": "^3.1.7" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" } }, "node_modules/@balena/dockerignore": { @@ -44,12 +98,161 @@ "kuler": "^2.0.0" } }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -63,6 +266,178 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-jsx-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", + "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn-loose": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz", + "integrity": "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -119,6 +494,20 @@ ], "license": "MIT" }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -128,6 +517,34 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bcrypt/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -185,6 +602,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -226,6 +656,46 @@ "node": ">= 0.8" } }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -244,18 +714,99 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/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/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/child_process": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==", "license": "ISC" }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -266,11 +817,24 @@ "color-string": "^1.6.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==", - "license": "MIT" + "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": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/color-string": { "version": "1.9.1", @@ -282,6 +846,15 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/color/node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -307,12 +880,27 @@ "text-hex": "1.0.x" } }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -334,9 +922,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -401,6 +989,30 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -417,6 +1029,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -425,6 +1043,71 @@ "node": ">= 0.8" } }, + "node_modules/dependency-cruiser": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.5.0.tgz", + "integrity": "sha512-6IELC3qRumlwhnbPLmcOK6WWdiGPFBw9a+D8DUsnTFpZ81tEtkAud4OPmU3OJFcuWS5VpgvKlctFkby5XDsGzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.13.0", + "acorn-jsx": "^5.3.2", + "acorn-jsx-walk": "^2.0.0", + "acorn-loose": "^8.4.0", + "acorn-walk": "^8.3.4", + "ajv": "^8.17.1", + "commander": "^12.1.0", + "enhanced-resolve": "^5.17.1", + "ignore": "^6.0.2", + "interpret": "^3.1.1", + "is-installed-globally": "^1.0.0", + "json5": "^2.2.3", + "memoize": "^10.0.0", + "picocolors": "^1.1.1", + "picomatch": "^4.0.2", + "prompts": "^2.4.2", + "rechoir": "^0.8.0", + "safe-regex": "^2.1.1", + "semver": "^7.6.3", + "teamcity-service-messages": "^0.1.14", + "tsconfig-paths-webpack-plugin": "^4.1.0", + "watskeburt": "^4.1.0" + }, + "bin": { + "depcruise": "bin/dependency-cruise.mjs", + "depcruise-baseline": "bin/depcruise-baseline.mjs", + "depcruise-fmt": "bin/depcruise-fmt.mjs", + "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs", + "dependency-cruise": "bin/dependency-cruise.mjs", + "dependency-cruiser": "bin/dependency-cruise.mjs" + }, + "engines": { + "node": "^18.17||>=20" + } + }, + "node_modules/dependency-cruiser/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/dependency-cruiser/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -434,6 +1117,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/docker-modem": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", @@ -463,11 +1155,29 @@ "node": ">= 8.0" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", @@ -482,6 +1192,29 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -491,6 +1224,37 @@ "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -515,6 +1279,15 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -523,17 +1296,27 @@ "node": ">= 0.6" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -564,6 +1347,21 @@ "node": ">= 0.10.0" } }, + "node_modules/express-rate-limit": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz", + "integrity": "sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "4 || 5 || ^5.0.0-beta.1" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -579,6 +1377,20 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -608,6 +1420,25 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -679,12 +1510,39 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, "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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -693,6 +1551,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -711,6 +1590,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -732,6 +1617,45 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-directory/node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -743,6 +1667,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -776,6 +1717,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -787,6 +1734,13 @@ "node": ">= 0.4" } }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "license": "BSD-2-Clause", + "optional": true + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -802,6 +1756,44 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -833,6 +1825,50 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", + "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -850,21 +1886,166 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -877,12 +2058,92 @@ "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==", + "license": "ISC", + "optional": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js-yaml/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT", + "optional": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, "node_modules/logform": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", @@ -900,6 +2161,71 @@ "node": ">= 12.0.0" } }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -908,6 +2234,22 @@ "node": ">= 0.6" } }, + "node_modules/memoize": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.0.0.tgz", + "integrity": "sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/memoize?sponsor=1" + } + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -957,6 +2299,31 @@ "node": ">= 0.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -969,6 +2336,122 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -988,6 +2471,12 @@ "license": "MIT", "optional": true }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -997,6 +2486,24 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.71.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", + "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -1034,6 +2541,102 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1083,6 +2686,29 @@ "fn.name": "1.x.x" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1100,11 +2726,99 @@ "node": ">=0.10.0" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-to-regexp": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1118,6 +2832,13 @@ "node": ">= 0.10" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -1128,6 +2849,15 @@ "once": "^1.3.1" } }, + "node_modules/python-shell": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/python-shell/-/python-shell-5.0.0.tgz", + "integrity": "sha512-RUOOOjHLhgR1MIQrCtnEqz/HJ1RMZBIN+REnpSUrfft2bXqXy69fwJASVziWExfFXsR1bCY0TznnHooNsCo0/w==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -1164,6 +2894,21 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -1178,6 +2923,96 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1198,6 +3033,16 @@ ], "license": "MIT" }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, "node_modules/safe-stable-stringify": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", @@ -1213,6 +3058,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -1276,6 +3133,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -1297,30 +3160,142 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "license": "MIT", + "optional": true, "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 10.0.0", + "npm": ">= 3.0.0" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", "license": "MIT", + "optional": true, "dependencies": { - "is-arrayish": "^0.3.1" + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" } }, "node_modules/split-ca": { @@ -1335,6 +3310,30 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, "node_modules/ssh2": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz", @@ -1352,6 +3351,19 @@ "nan": "^2.18.0" } }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -1378,6 +3390,178 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", + "license": "Apache-2.0" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tar-fs": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", @@ -1406,12 +3590,50 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/teamcity-service-messages": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz", + "integrity": "sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w==", + "dev": true, + "license": "MIT" + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1420,6 +3642,22 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -1429,6 +3667,48 @@ "node": ">= 14.0.0" } }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz", + "integrity": "sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -1447,6 +3727,33 @@ "node": ">= 0.6" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1470,6 +3777,15 @@ "node": ">= 0.4.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1479,6 +3795,19 @@ "node": ">= 0.8" } }, + "node_modules/watskeburt": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-4.1.0.tgz", + "integrity": "sha512-KkY5H51ajqy9HYYI+u9SIURcWnqeVVhdH0I+ab6aXPGHfZYxgRCwnR6Lm3+TYB6jJVt5jFqw4GAKmwf1zHmGQw==", + "dev": true, + "license": "MIT", + "bin": { + "watskeburt": "dist/run-cli.js" + }, + "engines": { + "node": "^18||>=20" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -1488,10 +3817,51 @@ "node": ">= 8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/winston": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.14.2.tgz", - "integrity": "sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.15.0.tgz", + "integrity": "sha512-RhruH2Cj0bV0WgNL+lOfoUBI4DVfdUNjVnJGVovWZmrcKtrFTTRzgXYK2O9cymSGjrERCtaAeHwMNnUWXlwZow==", "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", @@ -1530,6 +3900,21 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/yamljs": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", @@ -1543,6 +3928,36 @@ "json2yaml": "bin/json2yaml", "yaml2json": "bin/yaml2json" } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 372caad0..bd38ba88 100644 --- a/package.json +++ b/package.json @@ -2,21 +2,33 @@ "name": "dockstatapi", "version": "1.0.0", "description": "API for docker hosts using dockerode", - "main": "dockerstatsapi.js", + "main": "server.js", "scripts": { - "start": "node dockstatapi.js", - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node server.js", + "dev": "nodemon server.js", + "offline": "OFFLINE=true nodemon server.js", + "dep": "bash ./utils/createDependencyGraph.sh" }, "keywords": [], "author": "Its4Nik", "license": "ISC", "dependencies": { + "bcrypt": "^5.1.1", "child_process": "^1.0.2", "cors": "^2.8.5", "dockerode": "^4.0.2", - "express": "^4.21.0", + "express": "^4.21.1", + "express-rate-limit": "^7.4.1", "node-fetch": "^3.3.2", - "winston": "^3.14.2", + "python-shell": "^5.0.0", + "sqlite3": "^5.1.7", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "winston": "^3.15.0", "yamljs": "^0.3.0" + }, + "devDependencies": { + "dependency-cruiser": "^16.5.0", + "nodemon": "^3.1.7" } -} \ No newline at end of file +} diff --git a/routes/auth/routes.js b/routes/auth/routes.js new file mode 100644 index 00000000..a0b217e4 --- /dev/null +++ b/routes/auth/routes.js @@ -0,0 +1,145 @@ +const express = require("express"); +const bcrypt = require("bcrypt"); +const fs = require("fs"); +const path = require("path"); +const logger = require("../../utils/logger"); +const router = express.Router(); +const passwordFile = path.join(__dirname, "../../middleware/password.json"); +const passwordBool = path.join(__dirname, "../../middleware/usePassword.txt"); +const saltRounds = 10; + +function setTrue() { + fs.writeFile(passwordBool, "true", "utf8", (err) => { + if (err) { + logger.error("Error writing to the file:", err); + return; + } + logger.info(`Status "true" has been written to the file.`); + }); +} + +function setFalse() { + fs.writeFile(passwordBool, "false", "utf8", (err) => { + if (err) { + logger.error("Error writing to the file:", err); + return; + } + logger.info(`Status "false" has been written to the file.`); + }); +} + +/** + * @swagger + * /auth/enable: + * post: + * summary: Enable authentication by setting a password + * tags: [Authentication] + * parameters: + * - name: password + * in: query + * required: true + * responses: + * 200: + * description: Authentication enabled. + * 400: + * description: Password is required. + * 500: + * description: Error saving password. + */ +router.post("/enable", (req, res) => { + fs.readFile(passwordBool, "utf8", (err, data) => { + const password = req.query.password; + if (err) { + logger.error("Error reading the file:", err); + return; + } + + const isAuthEnabled = data.trim() === "true"; + if (isAuthEnabled) { + logger.error( + "Passowrd Authentication is already enabled, please dactivate it first", + ); + return res.status(401).json({ + message: + "Passowrd Authentication is already enabled, please dactivate it first", + }); + } + + if (!password) { + return res.status(400).json({ message: "Password is required" }); + } + + bcrypt.genSalt(saltRounds, (err, salt) => { + if (err) { + logger.error("Error generating salt"); + return res.status(500).json({ message: "Error generating salt" }); + } + + bcrypt.hash(password, salt, (err, hash) => { + if (err) { + logger.error("Error hashing password"); + return res.status(500).json({ message: "Error hashing password" }); + } + + const passwordData = { hash, salt }; + fs.writeFile(passwordFile, JSON.stringify(passwordData), (err) => { + if (err) + return res.status(500).json({ message: "Error saving password" }); + setTrue(); + res.json({ message: "Authentication enabled" }); + }); + }); + }); + }); +}); + +/** + * @swagger + * /auth/disable: + * post: + * summary: Disable authentication by providing the existing password + * tags: [Authentication] + * parameters: + * - name: password + * in: query + * required: true + * responses: + * 200: + * description: Authentication disabled. + * 400: + * description: Password is required. + * 401: + * description: Invalid password. + * 500: + * description: Error disabling authentication. + */ +router.post("/disable", (req, res) => { + const password = req.query.password; + if (!password) { + logger.error("Password is required!"); + return res.status(400).json({ message: "Password is required" }); + } + + fs.readFile(passwordFile, "utf8", (err, data) => { + if (err) { + logger.error("Error reading password"); + return res.status(500).json({ message: "Error reading password" }); + } + + const storedData = JSON.parse(data); + bcrypt.compare(password, storedData.hash, (err, result) => { + if (err) { + logger.error("Error validating password"); + return res.status(500).json({ message: "Error validating password" }); + } + if (!result) { + logger.error("Invalid password"); + return res.status(401).json({ message: "Invalid password" }); + } + setFalse(); + res.json({ message: "Authentication disabled" }); + }); + }); +}); + +module.exports = router; diff --git a/routes/data/routes.js b/routes/data/routes.js new file mode 100644 index 00000000..adce8d79 --- /dev/null +++ b/routes/data/routes.js @@ -0,0 +1,111 @@ +const express = require("express"); +const router = express.Router(); +const db = require("../../config/db"); +const logger = require("../../utils/logger"); + +function formatRows(rows) { + return rows.reduce((acc, row, index) => { + acc[index] = JSON.parse(row.info); + return acc; + }, {}); +} + +/** + * @swagger + * /data/latest: + * get: + * summary: Retrieve the latest entry from the database + * tags: [Database queries] + * responses: + * 200: + * description: A JSON object containing the latest entry's 'info' data. + * content: + * application/json: + * schema: + * type: object + * example: + * name: "Container A" + * id: "abcd1234" + * cpu_usage: 30 + * mem_usage: 2048 + */ +router.get("/latest", (req, res) => { + db.get( + "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", + (err, row) => { + if (err) { + logger.error("Error fetching latest data:", err.message); + return res.status(500).json({ error: "Internal server error" }); + } + res.json(JSON.parse(row.info)); + }, + ); +}); + +/** + * @swagger + * /data/time/24h: + * get: + * summary: Retrieve entries from the last 24 hours from the database + * tags: [Database queries] + * responses: + * 200: + * description: A numbered array of 'info' JSON objects from the last 24 hours. + * content: + * application/json: + * schema: + * type: object + * example: + * 0: + * name: "Container A" + * id: "abcd1234" + * cpu_usage: 30 + * mem_usage: 2048 + * 1: + * name: "Container B" + * id: "efgh5678" + * cpu_usage: 45 + * mem_usage: 3072 + */ +router.get("/time/24h", (req, res) => { + const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + db.all( + "SELECT info FROM data WHERE timestamp >= ?", + [oneDayAgo], + (err, rows) => { + if (err) { + logger.error("Error fetching data from last 24 hours:", err.message); + return res.status(500).json({ error: "Internal server error" }); + } + res.json(formatRows(rows)); + }, + ); +}); + +/** + * @swagger + * /data/clear: + * delete: + * summary: Clear all entries from the database + * tags: [Database queries] + * responses: + * 200: + * description: A message indicating whether the database was cleared successfully. + * content: + * application/json: + * schema: + * type: object + * example: + * message: "Database cleared successfully." + */ +router.delete("/clear", (req, res) => { + db.run("DELETE FROM data", (err) => { + if (err) { + logger.error("Error clearing the database:", err.message); + return res.status(500).json({ error: "Internal server error" }); + } + res.json({ message: "Database cleared successfully" }); + }); +}); + +module.exports = router; diff --git a/routes/frontendController/routes.js b/routes/frontendController/routes.js new file mode 100644 index 00000000..986276fa --- /dev/null +++ b/routes/frontendController/routes.js @@ -0,0 +1,340 @@ +const express = require("express"); +const router = express.Router(); +const logger = require("../../utils/logger"); +const { + hideContainer, + unhideContainer, + addTagToContainer, + removeTagFromContainer, + pinContainer, + unpinContainer, +} = require("../../controllers/frontendConfiguration"); + +/** + * @swagger + * /frontend/hide/{containerName}: + * post: + * summary: Hide a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to hide + * responses: + * 200: + * description: Container hidden successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Hide a container +router.post("/hide/:containerName", async (req, res) => { + const { containerName } = req.params; + const target = containerName; + //console.log(target); + + try { + await hideContainer(target); + res.json({ success: true, message: `Container, ${target}, hidden.` }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +/** + * @swagger + * /frontend/unhide/{containerName}: + * post: + * summary: Unhide a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to unhide + * responses: + * 200: + * description: Container unhidden successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Unhide a container +router.post("/unhide/:containerName", async (req, res) => { + const { containerName } = req.params; + try { + await unhideContainer(containerName); + res.json({ success: true, message: "Container unhidden successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +/** + * @swagger + * /frontend/tag/{containerName}/{tag}: + * post: + * summary: Add a tag to a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to add tag to + * - in: path + * name: tag + * schema: + * type: string + * required: true + * description: The tag to add + * responses: + * 200: + * description: Tag added successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Add a tag to a container +router.post("/tag/:containerName/:tag", async (req, res) => { + const { containerName, tag } = req.params; + try { + await addTagToContainer(containerName, tag); + res.json({ success: true, message: "Tag added successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +/** + * @swagger + * /frontend/remove-tag/{containerName}/{tag}: + * post: + * summary: Remove a tag from a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to remove tag from + * - in: path + * name: tag + * schema: + * type: string + * required: true + * description: The tag to remove + * responses: + * 200: + * description: Tag removed successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Remove a tag from a container +router.post("/remove-tag/:containerName/:tag", async (req, res) => { + const { containerName, tag } = req.params; + try { + await removeTagFromContainer(containerName, tag); + res.json({ success: true, message: "Tag removed successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +/** + * @swagger + * /frontend/pin/{containerName}: + * post: + * summary: Pin a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to pin + * responses: + * 200: + * description: Container pinned successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Pin a container +router.post("/pin/:containerName", async (req, res) => { + const { containerName } = req.params; + try { + await pinContainer(containerName); + res.json({ success: true, message: "Container pinned successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +/** + * @swagger + * /frontend/unpin/{containerName}: + * post: + * summary: Unpin a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to unpin + * responses: + * 200: + * description: Container unpinned successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Unpin a container +router.post("/unpin/:containerName", async (req, res) => { + const { containerName } = req.params; + try { + await unpinContainer(containerName); + res.json({ success: true, message: "Container unpinned successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +module.exports = router; diff --git a/routes/getter/routes.js b/routes/getter/routes.js new file mode 100644 index 00000000..6c5fbc0d --- /dev/null +++ b/routes/getter/routes.js @@ -0,0 +1,334 @@ +const extractRelevantData = require("../../utils/extractHostData"); +const express = require("express"); +const router = express.Router(); +const { + writeOfflineLog, + readOfflineLog, +} = require("../../utils/writeOfflineLog"); +const { getDockerClient } = require("../../utils/dockerClient"); +const { fetchAllContainers } = require("../../utils/containerService"); +const { getCurrentSchedule } = require("../../controllers/scheduler"); +const logger = require("../../utils/logger"); +const path = require("path"); +const fs = require("fs"); + +/** + * @swagger + * /api/hosts: + * get: + * summary: Retrieve a list of all available Docker hosts + * tags: [Hosts] + * responses: + * 200: + * description: A JSON object containing an array of host names. + * content: + * application/json: + * schema: + * type: object + * properties: + * hosts: + * type: array + * items: + * type: string + * example: ["local", "remote1"] + */ + +router.get("/hosts", (req, res) => { + const config = require("../../config/dockerConfig.json"); + const hosts = config.hosts.map((host) => host.name); + logger.info("Fetching all available Docker hosts"); + res.status(200).json({ hosts }); +}); + +/** + * @swagger + * /api/host/{hostName}/stats: + * get: + * summary: Retrieve statistics for a specified Docker host + * tags: [Hosts] + * parameters: + * - name: hostName + * in: path + * required: true + * description: The name of the host for which to fetch statistics. + * schema: + * type: string + * responses: + * 200: + * description: A JSON object containing relevant statistics for the specified host. + * content: + * application/json: + * schema: + * type: object + * properties: + * hostName: + * type: string + * description: The name of the Docker host. + * info: + * type: object + * description: Information about the Docker host (e.g., storage, running containers). + * version: + * type: object + * description: Version details of the Docker installation on the host. + * 500: + * description: An error occurred while fetching host statistics. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: Error message detailing the issue encountered. + */ +router.get("/host/:hostName/stats", async (req, res) => { + const hostName = req.params.hostName; + logger.info(`Fetching stats for host: ${hostName}`); + if (process.env.OFFLINE === "true") { + logger.info("Fetching offline Host Stats"); + res.status(200).json(readOfflineLog); + } else { + try { + const docker = getDockerClient(hostName); + const info = await docker.info(); + const version = await docker.version(); + const relevantData = extractRelevantData({ hostName, info, version }); + + writeOfflineLog(JSON.stringify(relevantData)); + res.status(200).json(relevantData); + } catch (error) { + logger.error( + `Error fetching stats for host: ${hostName} - ${error.message || "Unknown error"}`, + ); + res.status(500).json({ + error: `Error fetching host stats: ${error.message || "Unknown error"}`, + }); + } + } +}); + +/** + * @swagger + * /api/containers: + * get: + * summary: Retrieve all Docker containers across all configured hosts + * tags: [Containers] + * responses: + * 200: + * description: A JSON object containing container data for all hosts. + * content: + * application/json: + * schema: + * type: object + * additionalProperties: + * type: object + * properties: + * name: + * type: string + * description: Name of the container. + * id: + * type: string + * description: Unique identifier for the container. + * hostName: + * type: string + * description: The host on which the container is running. + * state: + * type: string + * description: Current state of the container (e.g., running, exited). + * cpu_usage: + * type: number + * format: double + * description: CPU usage in nanoseconds. + * mem_usage: + * type: number + * description: Memory usage in bytes. + * mem_limit: + * type: number + * description: Memory limit in bytes. + * net_rx: + * type: number + * description: Total received bytes over the network. + * net_tx: + * type: number + * description: Total transmitted bytes over the network. + * current_net_rx: + * type: number + * description: Current received bytes over the network. + * current_net_tx: + * type: number + * description: Current transmitted bytes over the network. + * networkMode: + * type: string + * description: Network mode configured for the container. + * link: + * type: string + * description: Optional link to additional information. + * icon: + * type: string + * description: Optional icon representing the container. + * tags: + * type: string + * description: Optional tags associated with the container. + * 500: + * description: An error occurred while fetching container data. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: Error message detailing the issue encountered. + */ +router.get("/containers", async (req, res) => { + logger.info("Fetching all containers across all hosts"); + try { + const allContainerData = await fetchAllContainers(); + res.status(200).json(allContainerData); + } catch (error) { + logger.error(`Error fetching containers: ${error.message}`); + res.status(500).json({ error: "Failed to fetch containers" }); + } +}); + +/** + * @swagger + * /api/config: + * get: + * summary: Retrieve Docker configuration + * tags: [Configuration] + * responses: + * 200: + * description: A JSON object containing the Docker configuration. + * content: + * application/json: + * schema: + * type: object + * additionalProperties: true + * 500: + * description: An error occurred while loading the Docker configuration. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: Error message detailing the issue encountered. + */ +router.get("/config", async (req, res) => { + const configPath = path.join(__dirname, "../../config/dockerConfig.json"); + try { + const rawData = fs.readFileSync(configPath); + const jsonData = JSON.parse(rawData.toString()); + res.status(200).json(jsonData); + } catch (error) { + logger.error("Error loading dockerConfig.json: " + error.message); + res.status(500).json({ error: "Failed to load Docker configuration" }); + } +}); + +/** + * @swagger + * /api/current-schedule: + * get: + * summary: Get the current fetch schedule in seconds + * tags: [Configuration] + * responses: + * 200: + * description: Current fetch schedule retrieved successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * interval: + * type: integer + * description: Current fetch interval in seconds. + */ +router.get("/current-schedule", (req, res) => { + const currentSchedule = getCurrentSchedule(); + res.json(currentSchedule); +}); + +/** + * @swagger + * /api/status: + * get: + * summary: Check server status + * tags: [Misc] + * description: Returns a 200 status with an "up" message to indicate the server is up and running. Used for Healthchecks + * responses: + * 200: + * description: Server is running + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "up" + */ +router.get("/status", (req, res) => { + res.status(200).json({ status: "up" }); +}); + +/** + * @swagger + * /api/frontend-config: + * get: + * summary: Get Frontend Configuration + * tags: [Configuration] + * description: Retrieves the frontend configuration data. + * responses: + * 200: + * description: Success + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * name: + * type: string + * description: Container Name + * hidden: + * type: boolean + * description: Whether the container is hidden + * tags: + * type: array + * items: + * type: string + * description: Tags associated with the container + * pinned: + * type: boolean + * description: Whether the container is pinned + * 500: + * description: Internal Server Error + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: Error message + */ +router.get("/frontend-config", (req, res) => { + const configPath = path.join( + __dirname, + "../../data/frontendConfiguration.json", + ); + try { + const rawData = fs.readFileSync(configPath); + const jsonData = JSON.parse(rawData.toString()); + res.status(200).json(jsonData); + } catch (error) { + logger.error("Error loading frontendConfiguration.json: " + error.message); + res.status(500).json({ error: "Failed to load Frontend configuration" }); + } +}); + +module.exports = router; diff --git a/routes/setter/routes.js b/routes/setter/routes.js new file mode 100644 index 00000000..24ae2ad9 --- /dev/null +++ b/routes/setter/routes.js @@ -0,0 +1,145 @@ +const { + setFetchInterval, + parseInterval, +} = require("../../controllers/scheduler"); +const express = require("express"); +const router = express.Router(); +const path = require("path"); +const fs = require("fs"); +const logger = require("../../utils/logger"); + +/** + * @swagger + * /conf/addHost: + * put: + * summary: Add a new host to the Docker configuration + * tags: [Configuration] + * parameters: + * - name: name + * in: query + * required: true + * description: The name of the new host. + * - name: url + * in: query + * required: true + * description: The URL of the new host. + * - name: port + * in: query + * required: true + * description: The port of the new host. + * responses: + * 200: + * description: Host added successfully. + * 400: + * description: Bad request, invalid input. + * 500: + * description: An error occurred while adding the host. + */ +router.put("/addHost", async (req, res) => { + const name = req.query.name; + const url = req.query.url; + const port = req.query.port; + const configPath = path.join(__dirname, "../../config/dockerConfig.json"); + + if (!name || !url || !port) { + return res.status(400).json({ error: "Name, Port and URL are required." }); + } + + try { + const rawData = fs.readFileSync(configPath); + const config = JSON.parse(rawData); + + // Check for existing host + if (config.hosts.some((host) => host.name === name)) { + return res.status(400).json({ error: "Host already exists." }); + } + + config.hosts.push({ name, url, port }); + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + logger.info(`Added new host: ${name}`); + res.status(200).json({ message: "Host added successfully." }); + } catch (error) { + logger.error("Error adding host: " + error.message); + res.status(500).json({ error: "Failed to add host." }); + } +}); + +/** + * @swagger + * /conf/scheduler: + * put: + * summary: Set fetch interval for data fetching + * tags: [Configuration] + * parameters: + * - name: interval + * in: query + * required: true + * description: The new interval for fetching data, e.g., "6h 20m", "300s". + * responses: + * 200: + * description: Fetch interval set successfully. + * 400: + * description: Invalid interval format or out of range. + */ +router.put("/scheduler", (req, res) => { + const interval = req.query.interval; + const newInterval = parseInterval(interval); + + if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { + return res + .status(400) + .json({ error: "Interval must be between 5 minutes and 6 hours." }); + } + + setFetchInterval(newInterval); + res.json({ message: `Fetch interval set to ${interval}.` }); +}); + +/** + * @swagger + * /conf/removeHost: + * delete: + * summary: Remove a host from the Docker configuration + * tags: [Configuration] + * parameters: + * - name: hostName + * in: query + * required: true + * description: The name of the host to remove. + * responses: + * 200: + * description: Host removed successfully. + * 404: + * description: Host not found. + * 500: + * description: An error occurred while removing the host. + */ +router.delete("/removeHost", async (req, res) => { + const hostName = req.query.hostName; + const configPath = path.join(__dirname, "../../config/dockerConfig.json"); + + if (!hostName) { + return res.status(400).json({ error: "Host name is required." }); + } + + try { + const rawData = fs.readFileSync(configPath); + const config = JSON.parse(rawData); + + // Check for existing host + const hostIndex = config.hosts.findIndex((host) => host.name === hostName); + if (hostIndex === -1) { + return res.status(404).json({ error: "Host not found." }); + } + + config.hosts.splice(hostIndex, 1); + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + logger.info(`Removed host: ${hostName}`); + res.status(200).json({ message: "Host removed successfully." }); + } catch (error) { + logger.error("Error removing host: " + error.message); + res.status(500).json({ error: "Failed to remove host." }); + } +}); + +module.exports = router; diff --git a/scripts/install_apprise.sh b/scripts/install_apprise.sh deleted file mode 100644 index 7506d0e8..00000000 --- a/scripts/install_apprise.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -VENV_DIR="/api" - -apk update -apk add python3 py3-pip py3-virtualenv - -python3 -m venv "$VENV_DIR" - -. "$VENV_DIR/bin/activate" - -pip install apprise - -deactivate - -echo "Apprise has been successfully installed in the virtual environment." diff --git a/scripts/notify.sh b/scripts/notify.sh deleted file mode 100755 index 54dc2262..00000000 --- a/scripts/notify.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -NOTIFY_TYPE=$1 # ADD, REMOVE, EXIT, ANY -CONTAINER_ID=$2 # Container ID -CONTAINER_NAME=$3 # Container Name -HOST=$4 # Host Name -STATE=$5 # Current State - -ADD_MESSAGE="${ADD_MESSAGE:-🆕 Container Added: $CONTAINER_NAME ($CONTAINER_ID) on $HOST}" -REMOVE_MESSAGE="${REMOVE_MESSAGE:-🚫 Container Removed: $CONTAINER_NAME ($CONTAINER_ID) on $HOST}" -EXIT_MESSAGE="${EXIT_MESSAGE:-❌ Container Exited: $CONTAINER_NAME ($CONTAINER_ID) on $HOST}" -ANY_MESSAGE="${ANY_MESSAGE:-⚠️ Container State Changed: $CONTAINER_NAME ($CONTAINER_ID) on $HOST - New State: $STATE}" - -case "$NOTIFY_TYPE" in - ADD) - MESSAGE="$ADD_MESSAGE" - ;; - REMOVE) - MESSAGE="$REMOVE_MESSAGE" - ;; - EXIT) - MESSAGE="$EXIT_MESSAGE" - ;; - ANY) - MESSAGE="$ANY_MESSAGE" - ;; - *) - MESSAGE="Unknown action for $CONTAINER_NAME ($CONTAINER_ID) on $HOST" - ;; -esac - -if [[ ! -f ./config/apprise_config.yml ]]; then - echo -n "No Apprise configuration found, aborting." - exit 1 -fi - -# Send notification via Apprise - -### PYTHON ENVIRONMENT: ### -. /api/bin/activate - -apprise -b "$MESSAGE" --config ./config/apprise_config.yml - -deactivate -########################### - -exit 0 diff --git a/server.js b/server.js new file mode 100644 index 00000000..b7a2731c --- /dev/null +++ b/server.js @@ -0,0 +1,33 @@ +const express = require("express"); +const swaggerDocs = require("./swagger/swaggerDocs"); +const api = require("./routes/getter/routes"); +const conf = require("./routes/setter/routes"); +const auth = require("./routes/auth/routes"); +const data = require("./routes/data/routes"); +const frontend = require("./routes/frontendController/routes"); +const authMiddleware = require("./middleware/authMiddleware"); +const app = express(); +const logger = require("./utils/logger"); +const { scheduleFetch } = require("./controllers/scheduler"); +const { limiter } = require("./utils/rateLimiter"); + +const PORT = "7070"; + +app.use(express.json()); + +app.use("/api-docs", (req, res, next) => next()); + +swaggerDocs(app); +scheduleFetch(); + +// Routes +app.use("/api", authMiddleware, api); +app.use("/conf", authMiddleware, conf); +app.use("/auth", authMiddleware, auth); +app.use("/data", authMiddleware, data); +app.use("/frontend", authMiddleware, frontend); + +app.listen(PORT, () => { + logger.info(`Server is running on http://localhost:${PORT}`); + logger.info(`Swagger docs available at http://localhost:${PORT}/api-docs`); +}); diff --git a/swagger/swaggerDocs.js b/swagger/swaggerDocs.js new file mode 100644 index 00000000..57193722 --- /dev/null +++ b/swagger/swaggerDocs.js @@ -0,0 +1,10 @@ +const swaggerUi = require("swagger-ui-express"); +const swaggerJsdoc = require("swagger-jsdoc"); +const swaggerConfig = require("../config/swaggerConfig"); + +const swaggerDocs = (app) => { + const specs = swaggerJsdoc(swaggerConfig); + app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs)); +}; + +module.exports = swaggerDocs; diff --git a/utils/containerService.js b/utils/containerService.js new file mode 100644 index 00000000..eb078a55 --- /dev/null +++ b/utils/containerService.js @@ -0,0 +1,63 @@ +const config = require("../config/dockerConfig.json"); +const logger = require("./logger"); +const { getDockerClient } = require("./dockerClient"); + +async function fetchAllContainers() { + const allContainerData = {}; + + for (const hostConfig of config.hosts) { + const hostName = hostConfig.name; + try { + const docker = getDockerClient(hostName); + const containers = await docker.listContainers({ all: true }); + + allContainerData[hostName] = await Promise.all( + containers.map(async (container) => { + const containerInfo = await docker + .getContainer(container.Id) + .inspect(); + const containerStats = await docker + .getContainer(container.Id) + .stats({ stream: false }); + const cpuDelta = + containerStats.cpu_stats.cpu_usage.total_usage - + containerStats.precpu_stats.cpu_usage.total_usage; + const systemCpuDelta = + containerStats.cpu_stats.system_cpu_usage - + containerStats.precpu_stats.system_cpu_usage; + const cpuUsage = + systemCpuDelta > 0 + ? (cpuDelta / systemCpuDelta) * + containerStats.cpu_stats.online_cpus + : 0; + + return { + name: container.Names[0].replace("/", ""), + id: container.Id, + hostName: hostName, + state: container.State, + cpu_usage: cpuUsage * 1000000000, + mem_usage: containerStats.memory_stats.usage, + mem_limit: containerStats.memory_stats.limit, + net_rx: containerStats.networks?.eth0?.rx_bytes || 0, + net_tx: containerStats.networks?.eth0?.tx_bytes || 0, + current_net_rx: containerStats.networks?.eth0?.rx_bytes || 0, + current_net_tx: containerStats.networks?.eth0?.tx_bytes || 0, + networkMode: containerInfo.HostConfig.NetworkMode, + }; + }), + ); + } catch (error) { + logger.error( + `Error fetching containers for host: ${hostName} - ${error.message}`, + ); + allContainerData[hostName] = { + error: `Error fetching containers: ${error.message}`, + }; + } + } + + return allContainerData; +} + +module.exports = { fetchAllContainers }; diff --git a/utils/createDependencyGraph.sh b/utils/createDependencyGraph.sh new file mode 100755 index 00000000..3e75de0a --- /dev/null +++ b/utils/createDependencyGraph.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +TMP=$(mktemp) + +cat ./server.js | grep "./routes" | awk '{print $2,$4}' > $TMP + +while read line; do + target_route=$(echo "$line" | cut -d '"' -f2) + route=$(echo "$line" | awk '{print $1}') + + echo + echo "Route: $route" + echo ${target_route}.js + + + npx depcruise \ + -p cli-feedback \ + -T mermaid \ + -x "^node_modules|logger|.dependency-cruiser|path|fs" \ + -f ./misc/dependencyGraphs/mermaid-${route}.txt \ + ${target_route}.js + +done < <(cat $TMP) + +npx depcruise \ + -p cli-feedback \ + -T mermaid \ + -x "^node_modules|logger|.dependency-cruiser|path|fs" \ + -f ./misc/dependencyGraphs/mermaid-all.txt \ + ./ + +sleep 0.5 + +echo -e "\n========\n\n DONE\n\n========" diff --git a/utils/dockerClient.js b/utils/dockerClient.js new file mode 100644 index 00000000..3d691e50 --- /dev/null +++ b/utils/dockerClient.js @@ -0,0 +1,45 @@ +const Docker = require("dockerode"); +const fs = require("fs"); +const path = require("path"); +const logger = require("./logger"); + +// Function to dynamically load config on each request +function loadDockerConfig() { + const configPath = path.join(__dirname, "../config/dockerConfig.json"); + try { + const rawData = fs.readFileSync(configPath); + logger.debug("Refreshed DockerConfig.json"); + return JSON.parse(rawData); + } catch (error) { + logger.error("Error loading dockerConfig.json: " + error.message); + throw new Error("Failed to load Docker configuration"); + } +} + +// Function to create the Docker client using separate url and port +function createDockerClient(hostConfig) { + logger.info( + `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port}`, + ); + return new Docker({ + host: hostConfig.url, + port: hostConfig.port || 2375, // Use 2375 as default port for non-TLS + protocol: "http", // Ensure the use of http for non-TLS + }); +} + +// This function will get the Docker client based on the host configuration +const getDockerClient = (hostName) => { + logger.debug(`Getting Docker Client for ${hostName}`); + const config = loadDockerConfig(); // Dynamically load config + const hostConfig = config.hosts.find((host) => host.name === hostName); + + if (!hostConfig) { + const errorMsg = `Docker host ${hostName} not found in configuration`; + logger.error(errorMsg); + throw new Error(errorMsg); + } + return createDockerClient(hostConfig); +}; + +module.exports = { getDockerClient }; diff --git a/utils/extractHostData.js b/utils/extractHostData.js new file mode 100644 index 00000000..87db239f --- /dev/null +++ b/utils/extractHostData.js @@ -0,0 +1,26 @@ +function extractRelevantData(jsonData) { + return { + hostName: jsonData.hostName, + info: { + ID: jsonData.info.ID, + Containers: jsonData.info.Containers, + ContainersRunning: jsonData.info.ContainersRunning, + ContainersPaused: jsonData.info.ContainersPaused, + ContainersStopped: jsonData.info.ContainersStopped, + Images: jsonData.info.Images, + OperatingSystem: jsonData.info.OperatingSystem, + KernelVersion: jsonData.info.KernelVersion, + Architecture: jsonData.info.Architecture, + MemTotal: jsonData.info.MemTotal, + NCPU: jsonData.info.NCPU, + }, + version: { + Components: jsonData.version.Components.reduce((acc, component) => { + acc[component.Name] = component.Version; + return acc; + }, {}), + }, + }; +} + +module.exports = extractRelevantData; diff --git a/utils/logger.js b/utils/logger.js new file mode 100644 index 00000000..853ca6fc --- /dev/null +++ b/utils/logger.js @@ -0,0 +1,20 @@ +const winston = require("winston"); +const loggerConfig = require("../config/loggerConfig"); + +const transports = [new winston.transports.Console()]; + +if (loggerConfig.transports.file.enabled) { + transports.push( + new winston.transports.File({ + filename: loggerConfig.transports.file.filename, + }), + ); +} + +const logger = winston.createLogger({ + level: loggerConfig.level, + format: loggerConfig.format, + transports, +}); + +module.exports = logger; diff --git a/utils/rateLimiter.js b/utils/rateLimiter.js new file mode 100644 index 00000000..c323c581 --- /dev/null +++ b/utils/rateLimiter.js @@ -0,0 +1,8 @@ +import { rateLimit } from "express-rate-limit"; + +export const limiter = rateLimit({ + windowMs: 5 * 60 * 1000, // 5 minutes + limit: 300, // Limit each IP to 300 requests per `window` (here, per 5 minutes) + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers +}); diff --git a/utils/writeOfflineLog.js b/utils/writeOfflineLog.js new file mode 100644 index 00000000..4d26b1d5 --- /dev/null +++ b/utils/writeOfflineLog.js @@ -0,0 +1,31 @@ +const fs = require("fs"); +const path = require("path"); +const logger = require("../utils/logger"); + +const LOG_FILE_PATH = path.join(__dirname, "../logs/hostStats.json"); + +function writeOfflineLog(message) { + try { + if (!fs.existsSync(LOG_FILE_PATH)) { + fs.writeFileSync(LOG_FILE_PATH, message); + } + } catch (error) { + logger.error("Error writing one time reference log: ", error); + } +} + +function readOfflineLog() { + fs.readFile(LOG_FILE_PATH, "utf-8", (err, data) => { + if (err) { + logger.error("Error reading offline log:", err); + } + + logger.debug("Returning data:", data); + return data; + }); +} + +module.exports = { + writeOfflineLog, + readOfflineLog, +}; From e2f3a9364d519b5ec075e6a94102acddfbb831cb Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 1 Nov 2024 19:20:20 +0100 Subject: [PATCH 002/369] Update build-dev.yaml --- .github/workflows/build-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index a8d55f2c..d3b13356 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -34,7 +34,7 @@ jobs: tags: | type=sha,format=long,prefix= flavor: | - type=schedule,pattern=nightly + type=pep440,pattern={{version}},value=nightly - name: Build and push uses: docker/build-push-action@v5 From 14c0951954286eb981cb4d5a25deba235542215f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 1 Nov 2024 19:21:42 +0100 Subject: [PATCH 003/369] Update build-dev.yaml --- .github/workflows/build-dev.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index d3b13356..cce888e2 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -32,8 +32,6 @@ jobs: with: images: ghcr.io/${{ github.repository }} tags: | - type=sha,format=long,prefix= - flavor: | type=pep440,pattern={{version}},value=nightly - name: Build and push From f4d8ec10e4a045380da9dc9b51e21fbf97466d7a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 1 Nov 2024 19:25:41 +0100 Subject: [PATCH 004/369] Update build-dev.yaml --- .github/workflows/build-dev.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index cce888e2..9833ede7 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -32,7 +32,9 @@ jobs: with: images: ghcr.io/${{ github.repository }} tags: | - type=pep440,pattern={{version}},value=nightly + type=raw,enable=true,priority=200,prefix=,suffix=,value=nightly + flavor: | + latest=false - name: Build and push uses: docker/build-push-action@v5 From 5d6c61c52af3e67fc5f9bc6c12ec3bed41eb029f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 1 Nov 2024 19:29:30 +0100 Subject: [PATCH 005/369] Add rate limiter --- server.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server.js b/server.js index b7a2731c..9ccb80d9 100644 --- a/server.js +++ b/server.js @@ -21,11 +21,11 @@ swaggerDocs(app); scheduleFetch(); // Routes -app.use("/api", authMiddleware, api); -app.use("/conf", authMiddleware, conf); -app.use("/auth", authMiddleware, auth); -app.use("/data", authMiddleware, data); -app.use("/frontend", authMiddleware, frontend); +app.use("/api", authMiddleware, limiter, api); +app.use("/conf", authMiddleware, limiter, conf); +app.use("/auth", authMiddleware, limiter, auth); +app.use("/data", authMiddleware, limiter, data); +app.use("/frontend", authMiddleware, limiter, frontend); app.listen(PORT, () => { logger.info(`Server is running on http://localhost:${PORT}`); From 68bb589f6756a06081b05014e38aae0c830852df Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 1 Nov 2024 20:13:12 +0100 Subject: [PATCH 006/369] Move rate limiter to middleware --- {utils => middleware}/rateLimiter.js | 0 server.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {utils => middleware}/rateLimiter.js (100%) diff --git a/utils/rateLimiter.js b/middleware/rateLimiter.js similarity index 100% rename from utils/rateLimiter.js rename to middleware/rateLimiter.js diff --git a/server.js b/server.js index 9ccb80d9..f6a86f39 100644 --- a/server.js +++ b/server.js @@ -9,7 +9,7 @@ const authMiddleware = require("./middleware/authMiddleware"); const app = express(); const logger = require("./utils/logger"); const { scheduleFetch } = require("./controllers/scheduler"); -const { limiter } = require("./utils/rateLimiter"); +const { limiter } = require("./middleware/rateLimiter"); const PORT = "7070"; From 6123267a33b03976661090374e83399592e9f945 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 1 Nov 2024 21:02:36 +0100 Subject: [PATCH 007/369] Formating and details on swagger page --- config/db.js | 38 +++++++------- config/loggerConfig.js | 35 +++++++------ config/swaggerConfig.js | 57 ++++++++++----------- data/database.db | Bin 16384 -> 126976 bytes misc/dependencyGraphs/mermaid-all.txt | 70 +++++++++++++------------- package.json | 4 +- 6 files changed, 104 insertions(+), 100 deletions(-) diff --git a/config/db.js b/config/db.js index 9317ab40..51850d3e 100644 --- a/config/db.js +++ b/config/db.js @@ -1,19 +1,19 @@ -const sqlite3 = require('sqlite3').verbose(); -const logger = require('./../utils/logger'); -const path = require('path'); -const dbPath = path.join(__dirname, '../data/database.db'); - -const db = new sqlite3.Database(dbPath, (err) => { - if (err) { - logger.error('Error opening database:', err.message); - } else { - db.run(`CREATE TABLE IF NOT EXISTS data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - info TEXT NOT NULL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP - )`); - logger.info('Database created / opened succesfully'); - } -}); - -module.exports = db; \ No newline at end of file +const sqlite3 = require("sqlite3").verbose(); +const logger = require("./../utils/logger"); +const path = require("path"); +const dbPath = path.join(__dirname, "../data/database.db"); + +const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + logger.error("Error opening database:", err.message); + } else { + db.run(`CREATE TABLE IF NOT EXISTS data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + info TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + )`); + logger.info("Database created / opened succesfully"); + } +}); + +module.exports = db; diff --git a/config/loggerConfig.js b/config/loggerConfig.js index 79503488..0f7641af 100644 --- a/config/loggerConfig.js +++ b/config/loggerConfig.js @@ -1,16 +1,19 @@ -const { format } = require('winston'); - -module.exports = { - level: 'info', - format: format.combine( - format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - format.printf(({ timestamp, level, message }) => `${timestamp} [${level.toUpperCase()}]: ${message}`) - ), - transports: { - console: true, - file: { - enabled: true, - filename: 'logs/app.log', - }, - }, -}; +const { format } = require("winston"); + +module.exports = { + level: "info", + format: format.combine( + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + format.printf( + ({ timestamp, level, message }) => + `${timestamp} [${level.toUpperCase()}]: ${message}`, + ), + ), + transports: { + console: true, + file: { + enabled: true, + filename: "logs/app.log", + }, + }, +}; diff --git a/config/swaggerConfig.js b/config/swaggerConfig.js index c79ae476..723897fc 100644 --- a/config/swaggerConfig.js +++ b/config/swaggerConfig.js @@ -1,28 +1,29 @@ -const options = { - definition: { - openapi: '3.0.0', - info: { - title: 'Your API Documentation', - version: '1.0.0', - description: 'API documentation with authentication', - }, - components: { - securitySchemes: { - passwordAuth: { - type: 'apiKey', - in: 'header', - name: 'x-password', - description: 'Password required for authentication', - }, - }, - }, - security: [ - { - passwordAuth: [], - }, - ], - }, - apis: ['./routes/*/*.js'], // Point to your route files -}; - -module.exports = options; +const options = { + definition: { + failOnErrors: true, + openapi: "3.0.0", + info: { + title: "DockStatAPI", + version: "2", + description: "An API used to query muliple docker hosts", + }, + components: { + securitySchemes: { + passwordAuth: { + type: "apiKey", + in: "header", + name: "x-password", + description: "Password required for authentication", + }, + }, + }, + security: [ + { + passwordAuth: [], + }, + ], + }, + apis: ["./routes/*/*.js"], +}; + +module.exports = options; diff --git a/data/database.db b/data/database.db index 80295980fc1caba1cff16d304197ec55893652f2..619beed4fb9bfcf112978dac7d78da14ad6617c9 100644 GIT binary patch literal 126976 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 delta 54 zcmZp8z~0cnI6<0~iGhKEWuk&TBh$u&1^k-=SOr-5zccWE-z=!`lz(CX2PZotvnXdu KVoBm60|5Y$9u8>$ diff --git a/misc/dependencyGraphs/mermaid-all.txt b/misc/dependencyGraphs/mermaid-all.txt index 87ba7da1..0fc2d66d 100644 --- a/misc/dependencyGraphs/mermaid-all.txt +++ b/misc/dependencyGraphs/mermaid-all.txt @@ -14,33 +14,33 @@ end subgraph 5["utils"] 6["dockerClient.js"] 8["containerService.js"] -N["extractHostData.js"] -O["writeOfflineLog.js"] -U["rateLimiter.js"] +O["extractHostData.js"] +P["writeOfflineLog.js"] end subgraph C["middleware"] D["authMiddleware.js"] +E["rateLimiter.js"] end -subgraph E["routes"] -subgraph F["auth"] -G["routes.js"] +subgraph F["routes"] +subgraph G["auth"] +H["routes.js"] end -subgraph H["data"] -I["routes.js"] +subgraph I["data"] +J["routes.js"] end -subgraph J["frontendController"] -K["routes.js"] +subgraph K["frontendController"] +L["routes.js"] end -subgraph L["getter"] -M["routes.js"] +subgraph M["getter"] +N["routes.js"] end -subgraph P["setter"] -Q["routes.js"] +subgraph Q["setter"] +R["routes.js"] end end -R["server.js"] -subgraph S["swagger"] -T["swaggerDocs.js"] +S["server.js"] +subgraph T["swagger"] +U["swaggerDocs.js"] end 4-->6 7-->1 @@ -49,22 +49,22 @@ end 8-->6 B-->1 B-->7 -I-->1 -K-->A -M-->9 -M-->B -M-->8 -M-->6 -M-->N -M-->O -Q-->B +J-->1 +L-->A +N-->9 +N-->B +N-->8 +N-->6 +N-->O +N-->P R-->B -R-->D -R-->G -R-->I -R-->K -R-->M -R-->Q -R-->T -R-->U -T-->2 +S-->B +S-->D +S-->E +S-->H +S-->J +S-->L +S-->N +S-->R +S-->U +U-->2 diff --git a/package.json b/package.json index bd38ba88..11077856 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dockstatapi", - "version": "1.0.0", + "version": "2", "description": "API for docker hosts using dockerode", "main": "server.js", "scripts": { @@ -11,7 +11,7 @@ }, "keywords": [], "author": "Its4Nik", - "license": "ISC", + "license": "BSD 3-Clause License", "dependencies": { "bcrypt": "^5.1.1", "child_process": "^1.0.2", From aa4a20fd5f4af1f24759754b09e52bd3053af205 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 2 Nov 2024 19:07:23 +0100 Subject: [PATCH 008/369] Added icon and url routes + adjustment of methods used when removinf information from /frontend/... --- controllers/frontendConfiguration.js | 118 ++++++++- data/database.db | Bin 126976 -> 577536 bytes routes/frontendController/routes.js | 347 +++++++++++++++++++++++---- server.js | 14 +- 4 files changed, 426 insertions(+), 53 deletions(-) diff --git a/controllers/frontendConfiguration.js b/controllers/frontendConfiguration.js index ff1ce3ea..2ba90e8c 100644 --- a/controllers/frontendConfiguration.js +++ b/controllers/frontendConfiguration.js @@ -2,7 +2,13 @@ const fs = require("fs"); const path = require("path"); const dataPath = path.join(__dirname, "../data/frontendConfiguration.json"); const logger = require("../utils/logger"); +const { PythonShellErrorWithLogs } = require("python-shell"); +const expression = + "https?://(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)"; +const regex = new RegExp(expression); +/////////////////////////////////////////////////////////////// +// Hide Containers: async function hideContainer(containerName) { try { let data = await readData(); @@ -19,6 +25,7 @@ async function hideContainer(containerName) { } } catch (error) { logger.error(error); + throw new Error(error); } } @@ -36,9 +43,12 @@ async function unhideContainer(containerName) { } } catch (error) { logger.error(error); + throw new Error(error); } } +/////////////////////////////////////////////////////////////// +// Tag containers async function addTagToContainer(containerName, tag) { try { let data = await readData(); @@ -58,6 +68,7 @@ async function addTagToContainer(containerName, tag) { } } catch (error) { logger.error(error); + throw new Error(error); } } @@ -77,9 +88,12 @@ async function removeTagFromContainer(containerName, tag) { } } catch (error) { logger.error(error); + throw new Error(error); } } +/////////////////////////////////////////////////////////////// +// Pin containers async function pinContainer(containerName) { try { let data = await readData(); @@ -96,6 +110,7 @@ async function pinContainer(containerName) { } } catch (error) { logger.error(error); + throw new Error(error); } } @@ -113,9 +128,107 @@ async function unpinContainer(containerName) { } } catch (error) { logger.error(error); + throw new Error(error); } } +/////////////////////////////////////////////////////////////// +// Add/remove link from containers +async function setLink(containerName, link) { + if (link.match(regex)) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + data[containerIndex].link = `${link}`; + await saveData(data); + } else { + data.push({ name: containerName, link: `${link}` }); + await saveData(data); + } + } catch (error) { + logger.error(error); + throw new Error(error); + } + } else { + logger.error(`Provided link is not valid: ${link}`); + throw new Error(`Provided link is not valid: ${link}`); + } +} + +async function removeLink(containerName) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + delete data[containerIndex].link; + await saveData(data); + cleanupData(); + } + } catch (error) { + logger.error(error); + throw new Error(error); + } +} + +/////////////////////////////////////////////////////////////// +// Add/remove icon from containers +async function setIcon(containerName, icon, custom) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (custom === true) { + if (containerIndex !== -1) { + data[containerIndex].icon = `custom/${icon}`; + await saveData(data); + } else { + data.push({ name: containerName, icon: `custom/${icon}` }); + await saveData(data); + } + } else { + if (containerIndex !== -1) { + data[containerIndex].icon = `${icon}`; + await saveData(data); + } else { + data.push({ name: containerName, icon: `${icon}` }); + await saveData(data); + } + } + } catch (error) { + logger.error(error); + throw new Error(error); + } +} + +async function removeIcon(containerName) { + try { + let data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, + ); + + if (containerIndex !== -1) { + delete data[containerIndex].icon; + await saveData(data); + cleanupData(); + } + } catch (error) { + logger.error(error); + throw new Error(error); + } +} + +/////////////////////////////////////////////////////////////// +// Data specific functionss async function readData() { try { const data = await fs.promises.readFile(dataPath, "utf-8"); @@ -176,5 +289,8 @@ module.exports = { removeTagFromContainer, pinContainer, unpinContainer, - cleanupData, + setLink, + removeLink, + setIcon, + removeIcon, }; diff --git a/data/database.db b/data/database.db index 619beed4fb9bfcf112978dac7d78da14ad6617c9..6535b160318c34abbe5c6debfa6291df728eb2f9 100644 GIT binary patch literal 577536 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

?G{#>66I&Ykj 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 diff --git a/routes/frontendController/routes.js b/routes/frontendController/routes.js index 986276fa..ed4f127f 100644 --- a/routes/frontendController/routes.js +++ b/routes/frontendController/routes.js @@ -8,13 +8,25 @@ const { removeTagFromContainer, pinContainer, unpinContainer, + setLink, + removeLink, + setIcon, + removeIcon, } = require("../../controllers/frontendConfiguration"); +/* +____ ___ ____ _____ +| _ \ / _ \/ ___|_ _| +| |_) | | | \___ \ | | +| __/| |_| |___) || | +|_| \___/|____/ |_| +*/ + /** * @swagger - * /frontend/hide/{containerName}: + * /frontend/show/{containerName}: * post: - * summary: Hide a container + * summary: Unhide a container * tags: [Frontend Configuration] * parameters: * - in: path @@ -22,10 +34,10 @@ const { * schema: * type: string * required: true - * description: The name of the container to hide + * description: The name of the container to unhide * responses: * 200: - * description: Container hidden successfully. + * description: Container unhidden successfully. * content: * application/json: * schema: @@ -51,15 +63,70 @@ const { * type: string * description: Error message */ -// Hide a container -router.post("/hide/:containerName", async (req, res) => { +// Unhide a container +router.post("/show/:containerName", async (req, res) => { const { containerName } = req.params; - const target = containerName; - //console.log(target); + try { + await unhideContainer(containerName); + res.json({ success: true, message: "Container unhidden successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); +/** + * @swagger + * /frontend/tag/{containerName}/{tag}: + * post: + * summary: Add a tag to a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to add tag to + * - in: path + * name: tag + * schema: + * type: string + * required: true + * description: The tag to add + * responses: + * 200: + * description: Tag added successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Add a tag to a container +router.post("/tag/:containerName/:tag", async (req, res) => { + const { containerName, tag } = req.params; try { - await hideContainer(target); - res.json({ success: true, message: `Container, ${target}, hidden.` }); + await addTagToContainer(containerName, tag); + res.json({ success: true, message: "Tag added successfully." }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } @@ -67,9 +134,9 @@ router.post("/hide/:containerName", async (req, res) => { /** * @swagger - * /frontend/unhide/{containerName}: + * /frontend/pin/{containerName}: * post: - * summary: Unhide a container + * summary: Pin a container * tags: [Frontend Configuration] * parameters: * - in: path @@ -77,10 +144,10 @@ router.post("/hide/:containerName", async (req, res) => { * schema: * type: string * required: true - * description: The name of the container to unhide + * description: The name of the container to pin * responses: * 200: - * description: Container unhidden successfully. + * description: Container pinned successfully. * content: * application/json: * schema: @@ -106,12 +173,12 @@ router.post("/hide/:containerName", async (req, res) => { * type: string * description: Error message */ -// Unhide a container -router.post("/unhide/:containerName", async (req, res) => { +// Pin a container +router.post("/pin/:containerName", async (req, res) => { const { containerName } = req.params; try { - await unhideContainer(containerName); - res.json({ success: true, message: "Container unhidden successfully." }); + await pinContainer(containerName); + res.json({ success: true, message: "Container pinned successfully." }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } @@ -119,9 +186,9 @@ router.post("/unhide/:containerName", async (req, res) => { /** * @swagger - * /frontend/tag/{containerName}/{tag}: + * /frontend/add-link/{containerName}/{link}: * post: - * summary: Add a tag to a container + * summary: Add a link to a container * tags: [Frontend Configuration] * parameters: * - in: path @@ -129,16 +196,16 @@ router.post("/unhide/:containerName", async (req, res) => { * schema: * type: string * required: true - * description: The name of the container to add tag to + * description: The name of the container to add link to * - in: path - * name: tag + * name: link * schema: * type: string * required: true - * description: The tag to add + * description: The link to add * responses: * 200: - * description: Tag added successfully. + * description: Link added successfully. * content: * application/json: * schema: @@ -164,12 +231,12 @@ router.post("/unhide/:containerName", async (req, res) => { * type: string * description: Error message */ -// Add a tag to a container -router.post("/tag/:containerName/:tag", async (req, res) => { - const { containerName, tag } = req.params; +// Add link to container +router.post("/add-link/:containerName/:link", async (req, res) => { + const { containerName, link } = req.params; try { - await addTagToContainer(containerName, tag); - res.json({ success: true, message: "Tag added successfully." }); + await setLink(containerName, link); + res.json({ success: true, message: "Link added successfully." }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } @@ -177,8 +244,138 @@ router.post("/tag/:containerName/:tag", async (req, res) => { /** * @swagger - * /frontend/remove-tag/{containerName}/{tag}: + * /frontend/add-icon/{containerName}/{icon}/{useCustomIcon}: * post: + * summary: Add an Icon to a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to add link to + * - in: path + * name: icon + * schema: + * type: string + * required: true + * description: The Icon to add + * - in: path + * name: useCustomIcon + * shema: + * type: boolean + * required: false + * description: If this icon is a custom icon or nor + * responses: + * 200: + * description: Icon added successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Add Icon to container +router.post( + "/add-icon/:containerName/:icon/:useCustomIcon", + async (req, res) => { + const { containerName, icon, useCustomIcon } = req.params; + try { + const custom = useCustomIcon === "true"; + + await setIcon(containerName, icon, custom); + res.json({ success: true, message: "Icon added successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } + }, +); + +/* + ____ _____ _ _____ _____ _____ +| _ \| ____| | | ____|_ _| ____| +| | | | _| | | | _| | | | _| +| |_| | |___| |___| |___ | | | |___ +|____/|_____|_____|_____| |_| |_____| +*/ + +/** + * @swagger + * /frontend/hide/{containerName}: + * delete: + * summary: Hide a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to hide + * responses: + * 200: + * description: Container hidden successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Hide a container +router.delete("/hide/:containerName", async (req, res) => { + const { containerName } = req.params; + const target = containerName; + try { + await hideContainer(target); + res.json({ success: true, message: `Container, ${target}, hidden.` }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +/** + * @swagger + * /frontend/remove-tag/{containerName}/{tag}: + * delete: * summary: Remove a tag from a container * tags: [Frontend Configuration] * parameters: @@ -223,7 +420,7 @@ router.post("/tag/:containerName/:tag", async (req, res) => { * description: Error message */ // Remove a tag from a container -router.post("/remove-tag/:containerName/:tag", async (req, res) => { +router.delete("/remove-tag/:containerName/:tag", async (req, res) => { const { containerName, tag } = req.params; try { await removeTagFromContainer(containerName, tag); @@ -235,9 +432,9 @@ router.post("/remove-tag/:containerName/:tag", async (req, res) => { /** * @swagger - * /frontend/pin/{containerName}: - * post: - * summary: Pin a container + * /frontend/unpin/{containerName}: + * delete: + * summary: Unpin a container * tags: [Frontend Configuration] * parameters: * - in: path @@ -245,10 +442,10 @@ router.post("/remove-tag/:containerName/:tag", async (req, res) => { * schema: * type: string * required: true - * description: The name of the container to pin + * description: The name of the container to unpin * responses: * 200: - * description: Container pinned successfully. + * description: Container unpinned successfully. * content: * application/json: * schema: @@ -274,12 +471,12 @@ router.post("/remove-tag/:containerName/:tag", async (req, res) => { * type: string * description: Error message */ -// Pin a container -router.post("/pin/:containerName", async (req, res) => { +// Unpin a container +router.delete("/unpin/:containerName", async (req, res) => { const { containerName } = req.params; try { - await pinContainer(containerName); - res.json({ success: true, message: "Container pinned successfully." }); + await unpinContainer(containerName); + res.json({ success: true, message: "Container unpinned successfully." }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } @@ -287,9 +484,9 @@ router.post("/pin/:containerName", async (req, res) => { /** * @swagger - * /frontend/unpin/{containerName}: - * post: - * summary: Unpin a container + * /frontend/remove-link/{containerName}: + * delete: + * summary: Remove a link from a container * tags: [Frontend Configuration] * parameters: * - in: path @@ -297,10 +494,10 @@ router.post("/pin/:containerName", async (req, res) => { * schema: * type: string * required: true - * description: The name of the container to unpin + * description: The name of the container to remove link from * responses: * 200: - * description: Container unpinned successfully. + * description: Link removed successfully. * content: * application/json: * schema: @@ -326,12 +523,64 @@ router.post("/pin/:containerName", async (req, res) => { * type: string * description: Error message */ -// Unpin a container -router.post("/unpin/:containerName", async (req, res) => { +// Remove link from container +router.delete("/remove-link/:containerName", async (req, res) => { const { containerName } = req.params; try { - await unpinContainer(containerName); - res.json({ success: true, message: "Container unpinned successfully." }); + await removeLink(containerName); + res.json({ success: true, message: "Link removed successfully." }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +/** + * @swagger + * /frontend/remove-icon/{containerName}: + * delete: + * summary: Remove an icon from a container + * tags: [Frontend Configuration] + * parameters: + * - in: path + * name: containerName + * schema: + * type: string + * required: true + * description: The name of the container to remove the icon from + * responses: + * 200: + * description: Icon removed successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * error: + * type: string + * description: Error message + */ +// Remove icon from container +router.delete("/remove-icon/:containerName", async (req, res) => { + const { containerName } = req.params; + try { + await removeIcon(containerName); + res.json({ success: true, message: "Icon removed successfully." }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } diff --git a/server.js b/server.js index f6a86f39..bf289f60 100644 --- a/server.js +++ b/server.js @@ -1,16 +1,24 @@ const express = require("express"); +const app = express(); + +// Utility: const swaggerDocs = require("./swagger/swaggerDocs"); +const logger = require("./utils/logger"); + +// Routes: const api = require("./routes/getter/routes"); const conf = require("./routes/setter/routes"); const auth = require("./routes/auth/routes"); const data = require("./routes/data/routes"); const frontend = require("./routes/frontendController/routes"); + +// Middleware: const authMiddleware = require("./middleware/authMiddleware"); -const app = express(); -const logger = require("./utils/logger"); -const { scheduleFetch } = require("./controllers/scheduler"); const { limiter } = require("./middleware/rateLimiter"); +// Controllers +const { scheduleFetch } = require("./controllers/scheduler"); + const PORT = "7070"; app.use(express.json()); From 0bbfc91e6d981665e2e28bbc2ee71b4d23ef4bcd Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 2 Nov 2024 19:16:58 +0100 Subject: [PATCH 009/369] Use commonjs for rate limit --- data/database.db | Bin 577536 -> 585728 bytes middleware/rateLimiter.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/data/database.db b/data/database.db index 6535b160318c34abbe5c6debfa6291df728eb2f9..caa9fbb0d7b4b30bfb4b2936cbb1e31f04e600b1 100644 GIT binary patch delta 1190 zcmZ8hO=w(I6rTI$&Ac15ah@}S4KdngtSE_(bMF70TC`vhTqz9!?IIm&5TqU2notpu zNx`8A+SEtAE?l%A7O@p#789f>U5nDL+_-QyZbU&Bu3UI#GRdTF-h=a<_ucQD@4K&a zt-pLC^2*Z6+p|q?m5|y>2-i)q z%AGcL&=NML9vOZDI#= zIeS-!TUVotkD{a&fcrEnYn)Twht@yQ6H{8!RDk0X3^?H=Lbzh0dOU#hwLj{7w|wvG zX#SI*^=T1 zKgLoe3@CxLUu}$4%I2#l?2Je2o%d#}YrsppHB!6K&!bvAXbJskK9!8>fqlk)D3KCW z;ofcjbX^*nd?zSUS~^I|eCGP#_Qhslt zROr7NS}(?5Rh*^uWn_XkU>zmz=%x5D3om_{Jhfj7w6Nz~(rplp%Ugd{V<0pfgkn7z zm3!<>{u_H(P$<0=84wxJrlk$P%$41gHC9-+*K9jKV4R3Rf5D_SjTTh~=bV%hgC#?W4 WLEe{mZP20C`Q3jT!yg2h@zH Date: Sat, 2 Nov 2024 19:27:48 +0100 Subject: [PATCH 010/369] Use ESM for rate limit; otherwise it wont work on my dev server (but locally?????) --- data/database.db | Bin 585728 -> 602112 bytes middleware/rateLimiter.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/data/database.db b/data/database.db index caa9fbb0d7b4b30bfb4b2936cbb1e31f04e600b1..ccc5cf7e392e87c9f58e2e912cedf258cbc1cd72 100644 GIT binary patch delta 2321 zcmZ9O-)|IE6vsQe^J@{>E<4*oTM*b51C+`A{iE0@Pxz)73Pc}ZLx`GywhF|Ei7m#) z5KziQt}kdXMuP!lX~Ggsgg%KN#wI|F8f9Na2>KUzW@fj$Yqxv1d%tI9&OPUQKev*)Lb_%v;-!plM`Hqs%nQP9u97In&6UM&1|5zjo?Trq`2%VM&=;J?<1ejwoi5 zo62=ZnDIxM$u1!%!y${uc&K*G_#;-gM2IOFs(6G!S6{s3Z?Y_5et*th;b5$Yh$fZ~ ztrQ$%#1+R{4zGE2?}1$h-dr<+rBaj=tj)wmKkyqdX4OTf=r_dZojdX*eawt+^C!)T zqgJOY5QhXzxyKt^K{XZ#zA4fwnydDgy;KQY+NVkjDYALTpwav*HS;A!hEZ9V}W9MP$8w)8xLWwcUuP{PjA?{* zD3DZ1Q>Yoe>gyuGQnuV%VoA7;Di;C=G9)<4Zz_7}Br$N$4d(t;zuUojgyGBAynUWE zX7+#YUCa}zC<%4)k&M;N&p0ENrOocU?$9!DoTi$H<|<%eigQzbkSV&UXy$T$Pd1+X zXOkt!!0GpSc8*LwE4*%CsCnBF4#Gp zCT8oo%ylDf`RjYA053Lk<6r^?EjS~x1s6&Uay0jR;4RE=bkCQ-CQvIDCiy{n?THD@ z6oQSBE%kHCz>o~J*p(0hULw-WJ@k8=L`}@sn#CSyE6{aUsJ7fd2b z%v7gULWD05_paf$)LA-DpzB4DoV zVL)db_n|E8p*}8QN1~P{Kq{09!IYUfWCc#5oB0ni0njs6%(d-yWjQPZe%fyPe-h;x zCQ=i|L!v0cO37;Nia!j9k1FpERENLum9>;g4nZnJ9B>{J2!QmhulguUIN0*x;*I~_MGibM{UfMx7g>p zA&iIMe>l?+ctNJ@$!eV(O(md@0H(_}G!UgkRQoIN8={$8=NGduYgXTM)GYeyYb#Hn z2jJxp%*wTbwwgcg`?_C&Tj20*Ax6v;c$`#G?MA@|79{~u<@La;D^dCH>*nbP{`w-| zyVZ&@R5&&B#)DX34X7y#QUN9Bmic*!J=qOg2F?N+0_PB Date: Tue, 5 Nov 2024 22:53:23 +0100 Subject: [PATCH 011/369] Telegram functionality and templating --- Dockerfile | 4 +- controllers/appriseController.js | 0 controllers/fetchData.js | 41 ++++++++++++++++- data/database.db | Bin 602112 -> 610304 bytes misc/entrypoint.sh | 0 package-lock.json | 17 +++++-- package.json | 9 ++++ routes/apprise/routes.js | 0 routes/frontendController/routes.js | 1 - utils/notifications/_test.js | 27 +++++++++++ utils/notifications/data/template.js | 61 +++++++++++++++++++++++++ utils/notifications/data/template.json | 3 ++ utils/notifications/mail.js | 26 +++++++++++ utils/notifications/telegram.js | 32 +++++++++++++ 14 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 controllers/appriseController.js mode change 100644 => 100755 misc/entrypoint.sh create mode 100644 routes/apprise/routes.js create mode 100644 utils/notifications/_test.js create mode 100644 utils/notifications/data/template.js create mode 100644 utils/notifications/data/template.json create mode 100644 utils/notifications/mail.js create mode 100644 utils/notifications/telegram.js diff --git a/Dockerfile b/Dockerfile index 5fc294e6..b23d93c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,9 @@ WORKDIR /api COPY --from=builder /api . -RUN apk add --no-cache bash curl +RUN apk add --no-cache \ + bash \ + curl EXPOSE 7070 diff --git a/controllers/appriseController.js b/controllers/appriseController.js new file mode 100644 index 00000000..e69de29b diff --git a/controllers/fetchData.js b/controllers/fetchData.js index 43b4f8a1..8df6b46f 100644 --- a/controllers/fetchData.js +++ b/controllers/fetchData.js @@ -3,6 +3,7 @@ const { fetchAllContainers } = require("../utils/containerService"); const logger = require("./../utils/logger"); const path = require("path"); const fs = require("fs"); +const { exec } = require("child_process"); const fetchData = async () => { try { @@ -12,7 +13,6 @@ const fetchData = async () => { if (process.env.OFFLINE === "true") { logger.info("No new data inserted --- OFFLINE MODE"); } else { - // Insert data into the SQLite database db.run( `INSERT INTO data (info) VALUES (?)`, [JSON.stringify(data)], @@ -26,6 +26,45 @@ const fetchData = async () => { }, ); } + + const containerStatus = {}; + Object.keys(allContainerData).forEach((host) => { + containerStatus[host] = allContainerData[host].map((container) => ({ + name: container.name, + id: container.id, + state: container.state, + host: container.hostName, + })); + }); + + const filePath = path.resolve(__dirname, "../data/states.json"); + let previousState = {}; + + if (fs.existsSync(filePath)) { + previousState = JSON.parse(fs.readFileSync(filePath, "utf8")); + } + + if (JSON.stringify(previousState) !== JSON.stringify(containerStatus)) { + fs.writeFileSync(filePath, JSON.stringify(containerStatus, null, 2)); + logger.info(`Container states saved to ${filePath}`); + + //TODO: rewrite every notification service using custom js modules + exec( + path.resolve(__dirname, "../misc/apprise.ppy"), + (error, stdout, stderr) => { + if (error) { + logger.error("Error executing apprise.py:", error.message); + return; + } + if (stderr) { + logger.warn("apprise.py stderr:", stderr); + } + logger.info("apprise.py executed successfully:", stdout); + }, + ); + } else { + logger.info("No state change detected, apprise.py not triggered."); + } } catch (error) { logger.error("Error fetching data:", error.message); } diff --git a/data/database.db b/data/database.db index ccc5cf7e392e87c9f58e2e912cedf258cbc1cd72..682c1a7450519549c99fb503d233e1e184c7bbb2 100644 GIT binary patch delta 32321 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 delta 21844 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 l;?0=ISI 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 M;dr<_z^Etx521eIv;Y7A diff --git a/misc/entrypoint.sh b/misc/entrypoint.sh old mode 100644 new mode 100755 diff --git a/package-lock.json b/package-lock.json index 68d93740..1ee8b135 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "dockstatapi", - "version": "1.0.0", + "version": "2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dockstatapi", - "version": "1.0.0", - "license": "ISC", + "version": "2", + "license": "BSD 3-Clause License", "dependencies": { "bcrypt": "^5.1.1", "child_process": "^1.0.2", @@ -15,7 +15,9 @@ "dockerode": "^4.0.2", "express": "^4.21.1", "express-rate-limit": "^7.4.1", + "js-yaml": "^4.1.0", "node-fetch": "^3.3.2", + "nodemailer": "^6.9.16", "python-shell": "^5.0.0", "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", @@ -2566,6 +2568,15 @@ "node": ">= 10.12.0" } }, + "node_modules/nodemailer": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", + "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", diff --git a/package.json b/package.json index 11077856..c6f82f2e 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "dockerode": "^4.0.2", "express": "^4.21.1", "express-rate-limit": "^7.4.1", + "js-yaml": "^4.1.0", "node-fetch": "^3.3.2", + "nodemailer": "^6.9.16", "python-shell": "^5.0.0", "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", @@ -30,5 +32,12 @@ "devDependencies": { "dependency-cruiser": "^16.5.0", "nodemon": "^3.1.7" + }, + "nodemonConfig": { + "ignore": [ + "**/logs/**", + "**/data/**" + ], + "delay": 2500 } } diff --git a/routes/apprise/routes.js b/routes/apprise/routes.js new file mode 100644 index 00000000..e69de29b diff --git a/routes/frontendController/routes.js b/routes/frontendController/routes.js index ed4f127f..de08c7a5 100644 --- a/routes/frontendController/routes.js +++ b/routes/frontendController/routes.js @@ -1,6 +1,5 @@ const express = require("express"); const router = express.Router(); -const logger = require("../../utils/logger"); const { hideContainer, unhideContainer, diff --git a/utils/notifications/_test.js b/utils/notifications/_test.js new file mode 100644 index 00000000..71398c7f --- /dev/null +++ b/utils/notifications/_test.js @@ -0,0 +1,27 @@ +const logger = require("../../utils/logger"); + +const { telegramNotification } = require("./telegram"); + +async function testNotification(type, containerId) { + if (!containerId) { + console.error("Container ID is required."); + return; + } + + switch (type) { + case "telegram": + logger.debug("Testing Telegram notification..."); + await telegramNotification(containerId); + break; + default: + logger.error("Unknown notification type. Use 'email' or 'telegram'."); + } +} + +if (require.main === module) { + const [type, containerId] = process.argv.slice(2); + testNotification(type, containerId); + console.log(`Testing ${type}, with: ${containerId}`); +} + +module.exports = testNotification; diff --git a/utils/notifications/data/template.js b/utils/notifications/data/template.js new file mode 100644 index 00000000..2bec652f --- /dev/null +++ b/utils/notifications/data/template.js @@ -0,0 +1,61 @@ +const fs = require("fs"); +const path = require("path"); + +const templatePath = path.join(__dirname, "template.json"); +const containersPath = path.join(__dirname, "../../../data/states.json"); + +function getTemplate() { + try { + const data = fs.readFileSync(templatePath, "utf8"); + return JSON.parse(data); + } catch (error) { + console.error("Failed to load template:", error); + return null; + } +} + +function setTemplate(newTemplate) { + try { + fs.writeFileSync( + templatePath, + JSON.stringify(newTemplate, null, 2), + "utf8", + ); + console.log("Template updated successfully"); + } catch (error) { + console.error("Failed to update template:", error); + } +} + +function renderTemplate(containerId) { + const template = getTemplate(); + if (!template) return null; + + try { + const data = fs.readFileSync(containersPath, "utf8"); + const containers = JSON.parse(data); + + let containerData = null; + for (const host in containers) { + containerData = containers[host].find((c) => c.id === containerId); + if (containerData) break; + } + + if (!containerData) { + console.error(`Container with ID ${containerId} not found`); + return null; + } + + // Substitute placeholders in the template with container data + return Object.keys(containerData).reduce( + (text, key) => + text.replace(new RegExp(`{{${key}}}`, "g"), containerData[key]), + template.text, + ); + } catch (error) { + console.error("Failed to load containers:", error); + return null; + } +} + +module.exports = { getTemplate, setTemplate, renderTemplate }; diff --git a/utils/notifications/data/template.json b/utils/notifications/data/template.json new file mode 100644 index 00000000..daa1f49d --- /dev/null +++ b/utils/notifications/data/template.json @@ -0,0 +1,3 @@ +{ + "text": "{{name}} ({{id}}) on {{host}} is {{state}}." +} diff --git a/utils/notifications/mail.js b/utils/notifications/mail.js new file mode 100644 index 00000000..24accb34 --- /dev/null +++ b/utils/notifications/mail.js @@ -0,0 +1,26 @@ +const nodemailer = require("nodemailer"); + +const transporter = nodemailer.createTransport({ + host: process.env.SMTP_SERVER_HOST, + port: process.env.SMTP_SERVER_PORT, + secure: process.env.SMTP_USE_SSL, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASSWORD, + }, +}); + +const mailOptions = { + from: "yourusername@email.com", + to: "yourfriend@email.com", + subject: "Sending Email using Node.js", + text: "That was easy!", +}; + +transporter.sendMail(mailOptions, function (error, info) { + if (error) { + console.log("Error:", error); + } else { + console.log("Email sent:", info.response); + } +}); diff --git a/utils/notifications/telegram.js b/utils/notifications/telegram.js new file mode 100644 index 00000000..5c79bdc8 --- /dev/null +++ b/utils/notifications/telegram.js @@ -0,0 +1,32 @@ +import fetch from "node-fetch"; +import logger from "../logger.js"; +import { renderTemplate } from "./data/template.js"; + +const telegram_bot_token = process.env.TELEGRAM_BOT_TOKEN; +const telegram_chat_id = process.env.TELEGRAM_CHAT_ID; + +export async function telegramNotification(containerId) { + const telegram_message = renderTemplate(containerId); + if (!telegram_message) { + logger.error("Failed to create notification message."); + return; + } + + try { + await fetch( + `https://api.telegram.org/bot${telegram_bot_token}/sendMessage`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + chat_id: telegram_chat_id, + text: telegram_message, + }), + }, + ); + } catch (error) { + logger.error("Error sending message:", error); + } +} From 2860402ccf38260d26583711fe9dd1a6743eefae Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 6 Nov 2024 00:52:06 +0100 Subject: [PATCH 012/369] More notification services and routes for testing and configuring the template --- config/loggerConfig.js | 21 ++- controllers/appriseController.js | 0 controllers/databaseMigration.js | 20 +++ controllers/scheduler.js | 21 ++- data/database.db | Bin 610304 -> 610304 bytes package-lock.json | 218 +++++++++++++++++++++++++ package.json | 1 + routes/apprise/routes.js | 0 routes/auth/routes.js | 3 +- routes/notifications/routes.js | 159 ++++++++++++++++++ server.js | 2 + utils/logger.js | 12 +- utils/notifications/_notify.js | 59 +++++++ utils/notifications/_test.js | 27 --- utils/notifications/data/template.js | 9 +- utils/notifications/data/template.json | 2 +- utils/notifications/discord.js | 27 +++ utils/notifications/email.js | 36 ++++ utils/notifications/mail.js | 26 --- utils/notifications/pushbullet.js | 30 ++++ utils/notifications/pushover.js | 30 ++++ utils/notifications/slack.js | 27 +++ utils/notifications/whatsapp.js | 29 ++++ 23 files changed, 678 insertions(+), 81 deletions(-) delete mode 100644 controllers/appriseController.js create mode 100644 controllers/databaseMigration.js delete mode 100644 routes/apprise/routes.js create mode 100644 routes/notifications/routes.js create mode 100644 utils/notifications/_notify.js delete mode 100644 utils/notifications/_test.js create mode 100644 utils/notifications/discord.js create mode 100644 utils/notifications/email.js delete mode 100644 utils/notifications/mail.js create mode 100644 utils/notifications/pushbullet.js create mode 100644 utils/notifications/pushover.js create mode 100644 utils/notifications/slack.js create mode 100644 utils/notifications/whatsapp.js diff --git a/config/loggerConfig.js b/config/loggerConfig.js index 0f7641af..38149ec4 100644 --- a/config/loggerConfig.js +++ b/config/loggerConfig.js @@ -1,19 +1,18 @@ -const { format } = require("winston"); +const { createLogger, format, transports } = require("winston"); -module.exports = { +const logger = createLogger({ level: "info", format: format.combine( format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), format.printf( ({ timestamp, level, message }) => - `${timestamp} [${level.toUpperCase()}]: ${message}`, + `[${timestamp}] ${level.toUpperCase()}: ${message}`, ), ), - transports: { - console: true, - file: { - enabled: true, - filename: "logs/app.log", - }, - }, -}; + transports: [ + new transports.Console(), + new transports.File({ filename: "logs/app.log" }), + ], +}); + +module.exports = logger; diff --git a/controllers/appriseController.js b/controllers/appriseController.js deleted file mode 100644 index e69de29b..00000000 diff --git a/controllers/databaseMigration.js b/controllers/databaseMigration.js new file mode 100644 index 00000000..263de07f --- /dev/null +++ b/controllers/databaseMigration.js @@ -0,0 +1,20 @@ +const db = require("../config/db"); +const logger = require("../utils/logger"); + +function clearOldEntries() { + const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1000; + + db.run( + `DELETE FROM data WHERE createdAt < ?`, + [twentyFourHoursAgo], + (err) => { + if (err) { + logger.error("Error deleting old entries:", err.message); + throw new Error("Database cleanup failed"); + } + logger.info("Old entries cleared successfully"); + }, + ); +} + +module.exports = clearOldEntries; diff --git a/controllers/scheduler.js b/controllers/scheduler.js index 5bb3ca7b..6322d327 100644 --- a/controllers/scheduler.js +++ b/controllers/scheduler.js @@ -1,28 +1,41 @@ +// path: controllers/scheduler.js + const fetchData = require("./fetchData"); const logger = require("../utils/logger"); const db = require("../config/db"); -let fetchInterval = 5 * 60 * 1000; +let fetchInterval = 5 * 60 * 1000; // Fetch data every 5 minutes by default let intervalId; +let cleanupIntervalId; const scheduleFetch = () => { fetchData().then(() => { cleanupOldEntries(); }); + intervalId = setInterval(() => { logger.info( `Fetching data at interval of ${fetchInterval / 1000} seconds.`, ); - cleanupOldEntries(); fetchData(); }, fetchInterval); + + // Schedule cleanup every 24 hours (86400000 ms) + cleanupIntervalId = setInterval( + () => { + cleanupOldEntries(); + }, + 24 * 60 * 60 * 1000, + ); + logger.info(`Data fetching scheduled every ${fetchInterval / 1000} seconds.`); + logger.info("Old entries cleanup scheduled every 24 hours."); }; const setFetchInterval = (newInterval) => { if (intervalId) { clearInterval(intervalId); - logger.info(`Cleared existing fetch interval.`); + logger.info("Cleared existing fetch interval."); } fetchInterval = newInterval; scheduleFetch(); @@ -61,7 +74,7 @@ const cleanupOldEntries = async () => { ).toISOString(); try { await db.run("DELETE FROM data WHERE timestamp < ?", twentyFourHoursAgo); - logger.info(`Old entries cleared from the database.`); + logger.info("Old entries cleared from the database."); } catch (error) { logger.error(`Error clearing old entries: ${error.message}`); } diff --git a/data/database.db b/data/database.db index 682c1a7450519549c99fb503d233e1e184c7bbb2..d36f65d65fdc3cfd4736d30e65395b6a212c3393 100644 GIT binary patch delta 36079 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 delta 35527 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 diff --git a/package-lock.json b/package-lock.json index 1ee8b135..f4dcffbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", + "twilio": "^5.3.5", "winston": "^3.15.0", "yamljs": "^0.3.0" }, @@ -470,6 +471,23 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -641,6 +659,12 @@ "ieee754": "^1.1.13" } }, + "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/buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -882,6 +906,18 @@ "text-hex": "1.0.x" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", @@ -974,6 +1010,12 @@ "node": ">= 12" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -1031,6 +1073,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -1169,6 +1220,15 @@ "node": ">=6.0.0" } }, + "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/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1477,6 +1537,40 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -2112,6 +2206,49 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "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": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "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": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -2134,18 +2271,60 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "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.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "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.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "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/logform": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", @@ -2843,6 +3022,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -3069,6 +3254,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scmp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", + "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==", + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -3726,6 +3917,24 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "license": "Unlicense" }, + "node_modules/twilio": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.3.5.tgz", + "integrity": "sha512-f/sA1Yd6TyIzfcq0u4QDGU+93afwswsJB+rf3T08tvBAMobBDVR3DfGREwJr5jp8xUic0qWa7GbJidk16NA4bg==", + "license": "MIT", + "dependencies": { + "axios": "^1.7.4", + "dayjs": "^1.11.9", + "https-proxy-agent": "^5.0.0", + "jsonwebtoken": "^9.0.2", + "qs": "^6.9.4", + "scmp": "^2.1.0", + "xmlbuilder": "^13.0.2" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3911,6 +4120,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index c6f82f2e..b1c3b7c4 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", + "twilio": "^5.3.5", "winston": "^3.15.0", "yamljs": "^0.3.0" }, diff --git a/routes/apprise/routes.js b/routes/apprise/routes.js deleted file mode 100644 index e69de29b..00000000 diff --git a/routes/auth/routes.js b/routes/auth/routes.js index a0b217e4..bbc20d4b 100644 --- a/routes/auth/routes.js +++ b/routes/auth/routes.js @@ -83,8 +83,9 @@ router.post("/enable", (req, res) => { const passwordData = { hash, salt }; fs.writeFile(passwordFile, JSON.stringify(passwordData), (err) => { - if (err) + if (err) { return res.status(500).json({ message: "Error saving password" }); + } setTrue(); res.json({ message: "Authentication enabled" }); }); diff --git a/routes/notifications/routes.js b/routes/notifications/routes.js new file mode 100644 index 00000000..592ab638 --- /dev/null +++ b/routes/notifications/routes.js @@ -0,0 +1,159 @@ +const express = require("express"); +const router = express.Router(); +const logger = require("../../utils/logger"); +const path = require("path"); +const fs = require("fs"); +const notify = require("../../utils/notifications/_notify"); +const dataTemplate = path.join( + __dirname, + "../../utils/notifications/data/template.json", +); +/** + * @swagger + * /notification-service/get-template: + * get: + * summary: Retrieve the notification template + * tags: [Notification Service] + * responses: + * 200: + * description: Template data retrieved successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful + * data: + * type: object + * description: The template data in JSON format + * 500: + * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: Error message + */ +router.get("/get-template", (req, res) => { + fs.readFile(dataTemplate, "utf-8", (error, data) => { + if (error) { + logger.error("Errored opening:", error); + return res.status(500).json({ message: `Error opening: ${error}` }); + } + res.json(JSON.parse(data)); + }); +}); + +/** + * @swagger + * /notification-service/set-template: + * post: + * summary: Update the notification template + * tags: [Notification Service] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * description: New template data to save + * responses: + * 200: + * description: Template updated successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: Success message + * 500: + * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: Error message + */ +router.post("/set-template", (req, res) => { + const newData = req.body; + + fs.writeFile( + dataTemplate, + JSON.stringify(newData, null, 2), + "utf-8", + (error) => { + if (error) { + logger.error("Errored writing to file:", error); + return res + .status(500) + .json({ message: `Error writing to file: ${error}` }); + } + res.json({ message: "Template updated successfully." }); + }, + ); +}); + +/** + * @swagger + * /notification-service/test/{type}/{containerId}: + * post: + * summary: Send a test notification for a specific container + * tags: [Notification Service] + * parameters: + * - in: path + * name: type + * schema: + * type: string + * required: true + * description: Type of notification to test + * - in: path + * name: containerId + * schema: + * type: string + * required: true + * description: The ID of the container for the notification test + * responses: + * 200: + * description: Test notification sent successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 500: + * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + */ +router.post("/test/:type/:containerId", async (req, res) => { + const { type, containerId } = req.params; + try { + await notify(type, containerId); + res.json({ success: true, message: `Sent test notification to ${type}` }); + } catch (error) { + res.json({ success: false, message: `Errored: ${error}` }); + } +}); + +module.exports = router; diff --git a/server.js b/server.js index bf289f60..a62b625d 100644 --- a/server.js +++ b/server.js @@ -11,6 +11,7 @@ const conf = require("./routes/setter/routes"); const auth = require("./routes/auth/routes"); const data = require("./routes/data/routes"); const frontend = require("./routes/frontendController/routes"); +const notificationService = require("./routes/notifications/routes"); // Middleware: const authMiddleware = require("./middleware/authMiddleware"); @@ -34,6 +35,7 @@ app.use("/conf", authMiddleware, limiter, conf); app.use("/auth", authMiddleware, limiter, auth); app.use("/data", authMiddleware, limiter, data); app.use("/frontend", authMiddleware, limiter, frontend); +app.use("/notification-service", authMiddleware, limiter, notificationService); app.listen(PORT, () => { logger.info(`Server is running on http://localhost:${PORT}`); diff --git a/utils/logger.js b/utils/logger.js index 853ca6fc..9d25e5d6 100644 --- a/utils/logger.js +++ b/utils/logger.js @@ -3,13 +3,11 @@ const loggerConfig = require("../config/loggerConfig"); const transports = [new winston.transports.Console()]; -if (loggerConfig.transports.file.enabled) { - transports.push( - new winston.transports.File({ - filename: loggerConfig.transports.file.filename, - }), - ); -} +transports.push( + new winston.transports.File({ + filename: "./logs/app.log", + }), +); const logger = winston.createLogger({ level: loggerConfig.level, diff --git a/utils/notifications/_notify.js b/utils/notifications/_notify.js new file mode 100644 index 00000000..b4a96fda --- /dev/null +++ b/utils/notifications/_notify.js @@ -0,0 +1,59 @@ +const logger = require("../../utils/logger"); + +const { telegramNotification } = require("./telegram"); +const { slackNotification } = require("./slack"); +const { discordNotification } = require("./discord"); +const { emailNotification } = require("./email"); +const { whatsappNotification } = require("./whatsapp"); +const { pushbulletNotification } = require("./pushbullet"); +const { pushoverNotification } = require("./pushover"); + +async function notify(type, containerId) { + if (!containerId) { + logger.error("Container ID is required."); + throw new Error("Container ID is required."); + } + + switch (type) { + case "telegram": + logger.debug("Testing Telegram notification..."); + await telegramNotification(containerId); + break; + case "slack": + logger.debug("Testing Slack notification..."); + await slackNotification(containerId); + break; + case "discord": + logger.debug("Testing Discord notification..."); + await discordNotification(containerId); + break; + case "email": + logger.debug("Testing Email notification..."); + await emailNotification(containerId); + break; + case "whatsapp": + logger.debug("Testing WhatsApp notification..."); + await whatsappNotification(containerId); + break; + case "pushbullet": + logger.debug("Testing Pushbullet notification..."); + await pushbulletNotification(containerId); + break; + case "pushover": + logger.debug("Testing Pushover notification..."); + await pushoverNotification(containerId); + break; + default: + const errorMsg = "Unknown notification type."; + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + +if (require.main === module) { + const [type, containerId] = process.argv.slice(2); + notify(type, containerId); + console.log(`Testing ${type}, with: ${containerId}`); +} + +module.exports = notify; diff --git a/utils/notifications/_test.js b/utils/notifications/_test.js deleted file mode 100644 index 71398c7f..00000000 --- a/utils/notifications/_test.js +++ /dev/null @@ -1,27 +0,0 @@ -const logger = require("../../utils/logger"); - -const { telegramNotification } = require("./telegram"); - -async function testNotification(type, containerId) { - if (!containerId) { - console.error("Container ID is required."); - return; - } - - switch (type) { - case "telegram": - logger.debug("Testing Telegram notification..."); - await telegramNotification(containerId); - break; - default: - logger.error("Unknown notification type. Use 'email' or 'telegram'."); - } -} - -if (require.main === module) { - const [type, containerId] = process.argv.slice(2); - testNotification(type, containerId); - console.log(`Testing ${type}, with: ${containerId}`); -} - -module.exports = testNotification; diff --git a/utils/notifications/data/template.js b/utils/notifications/data/template.js index 2bec652f..9a090f61 100644 --- a/utils/notifications/data/template.js +++ b/utils/notifications/data/template.js @@ -1,5 +1,6 @@ const fs = require("fs"); const path = require("path"); +const logger = require("../../logger"); const templatePath = path.join(__dirname, "template.json"); const containersPath = path.join(__dirname, "../../../data/states.json"); @@ -21,9 +22,9 @@ function setTemplate(newTemplate) { JSON.stringify(newTemplate, null, 2), "utf8", ); - console.log("Template updated successfully"); + logger.log("Template updated successfully"); } catch (error) { - console.error("Failed to update template:", error); + logger.error("Failed to update template:", error); } } @@ -50,10 +51,10 @@ function renderTemplate(containerId) { return Object.keys(containerData).reduce( (text, key) => text.replace(new RegExp(`{{${key}}}`, "g"), containerData[key]), - template.text, + template.message, ); } catch (error) { - console.error("Failed to load containers:", error); + logger.error("Failed to load containers:", error); return null; } } diff --git a/utils/notifications/data/template.json b/utils/notifications/data/template.json index daa1f49d..6a57d442 100644 --- a/utils/notifications/data/template.json +++ b/utils/notifications/data/template.json @@ -1,3 +1,3 @@ { "text": "{{name}} ({{id}}) on {{host}} is {{state}}." -} +} \ No newline at end of file diff --git a/utils/notifications/discord.js b/utils/notifications/discord.js new file mode 100644 index 00000000..c7bfe828 --- /dev/null +++ b/utils/notifications/discord.js @@ -0,0 +1,27 @@ +import fetch from "node-fetch"; +import logger from "../logger.js"; +import { renderTemplate } from "./data/template.js"; + +const discord_webhook_url = process.env.DISCORD_WEBHOOK_URL; + +export async function discordNotification(containerId) { + const discord_message = renderTemplate(containerId); + if (!discord_message) { + logger.error("Failed to create notification message."); + return; + } + + try { + await fetch(discord_webhook_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + content: discord_message, + }), + }); + } catch (error) { + logger.error("Error sending Discord message:", error); + } +} diff --git a/utils/notifications/email.js b/utils/notifications/email.js new file mode 100644 index 00000000..d7016795 --- /dev/null +++ b/utils/notifications/email.js @@ -0,0 +1,36 @@ +import nodemailer from "nodemailer"; +import logger from "../logger.js"; +import { renderTemplate } from "./data/template.js"; + +const email_sender = process.env.EMAIL_SENDER; +const email_recipient = process.env.EMAIL_RECIPIENT; +const email_password = process.env.EMAIL_PASSWORD; + +export async function emailNotification(containerId) { + const email_message = renderTemplate(containerId); + if (!email_message) { + logger.error("Failed to create notification message."); + return; + } + + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: email_sender, + pass: email_password, + }, + }); + + const mailOptions = { + from: email_sender, + to: email_recipient, + subject: "Container Notification", + text: email_message, + }; + + try { + await transporter.sendMail(mailOptions); + } catch (error) { + logger.error("Error sending email:", error); + } +} diff --git a/utils/notifications/mail.js b/utils/notifications/mail.js deleted file mode 100644 index 24accb34..00000000 --- a/utils/notifications/mail.js +++ /dev/null @@ -1,26 +0,0 @@ -const nodemailer = require("nodemailer"); - -const transporter = nodemailer.createTransport({ - host: process.env.SMTP_SERVER_HOST, - port: process.env.SMTP_SERVER_PORT, - secure: process.env.SMTP_USE_SSL, - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASSWORD, - }, -}); - -const mailOptions = { - from: "yourusername@email.com", - to: "yourfriend@email.com", - subject: "Sending Email using Node.js", - text: "That was easy!", -}; - -transporter.sendMail(mailOptions, function (error, info) { - if (error) { - console.log("Error:", error); - } else { - console.log("Email sent:", info.response); - } -}); diff --git a/utils/notifications/pushbullet.js b/utils/notifications/pushbullet.js new file mode 100644 index 00000000..442f44d0 --- /dev/null +++ b/utils/notifications/pushbullet.js @@ -0,0 +1,30 @@ +import fetch from "node-fetch"; +import logger from "../logger.js"; +import { renderTemplate } from "./data/template.js"; + +const pushbullet_access_token = process.env.PUSHBULLET_ACCESS_TOKEN; + +export async function pushbulletNotification(containerId) { + const pushbullet_message = renderTemplate(containerId); + if (!pushbullet_message) { + logger.error("Failed to create notification message."); + return; + } + + try { + await fetch("https://api.pushbullet.com/v2/pushes", { + method: "POST", + headers: { + "Access-Token": pushbullet_access_token, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + type: "note", + title: "Container Notification", + body: pushbullet_message, + }), + }); + } catch (error) { + logger.error("Error sending Pushbullet message:", error); + } +} diff --git a/utils/notifications/pushover.js b/utils/notifications/pushover.js new file mode 100644 index 00000000..592e7f09 --- /dev/null +++ b/utils/notifications/pushover.js @@ -0,0 +1,30 @@ +import fetch from "node-fetch"; +import logger from "../logger.js"; +import { renderTemplate } from "./data/template.js"; + +const pushover_user_key = process.env.PUSHOVER_USER_KEY; +const pushover_api_token = process.env.PUSHOVER_API_TOKEN; + +export async function pushoverNotification(containerId) { + const pushover_message = renderTemplate(containerId); + if (!pushover_message) { + logger.error("Failed to create notification message."); + return; + } + + try { + await fetch("https://api.pushover.net/1/messages.json", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + token: pushover_api_token, + user: pushover_user_key, + message: pushover_message, + }), + }); + } catch (error) { + logger.error("Error sending Pushover message:", error); + } +} diff --git a/utils/notifications/slack.js b/utils/notifications/slack.js new file mode 100644 index 00000000..2c1a67a2 --- /dev/null +++ b/utils/notifications/slack.js @@ -0,0 +1,27 @@ +import fetch from "node-fetch"; +import logger from "../logger.js"; +import { renderTemplate } from "./data/template.js"; + +const slack_webhook_url = process.env.SLACK_WEBHOOK_URL; + +export async function slackNotification(containerId) { + const slack_message = renderTemplate(containerId); + if (!slack_message) { + logger.error("Failed to create notification message."); + return; + } + + try { + await fetch(slack_webhook_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text: slack_message, + }), + }); + } catch (error) { + logger.error("Error sending Slack message:", error); + } +} diff --git a/utils/notifications/whatsapp.js b/utils/notifications/whatsapp.js new file mode 100644 index 00000000..d714b0b6 --- /dev/null +++ b/utils/notifications/whatsapp.js @@ -0,0 +1,29 @@ +import fetch from "node-fetch"; +import logger from "../logger.js"; +import { renderTemplate } from "./data/template.js"; + +const whatsapp_api_url = process.env.WHATSAPP_API_URL; // e.g., Twilio or other API service +const whatsapp_recipient = process.env.WHATSAPP_RECIPIENT; + +export async function whatsappNotification(containerId) { + const whatsapp_message = renderTemplate(containerId); + if (!whatsapp_message) { + logger.error("Failed to create notification message."); + return; + } + + try { + await fetch(whatsapp_api_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + to: whatsapp_recipient, + body: whatsapp_message, + }), + }); + } catch (error) { + logger.error("Error sending WhatsApp message:", error); + } +} From b8f501e0fd425e777aeaf7ad07de073dcd31a66c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 6 Nov 2024 01:14:45 +0100 Subject: [PATCH 013/369] Fix CodeQL --- controllers/fetchData.js | 18 ++---------------- controllers/frontendConfiguration.js | 3 +-- controllers/scheduler.js | 6 +----- data/database.db | Bin 610304 -> 610304 bytes server.js | 12 ++++++------ 5 files changed, 10 insertions(+), 29 deletions(-) diff --git a/controllers/fetchData.js b/controllers/fetchData.js index 8df6b46f..ba14c348 100644 --- a/controllers/fetchData.js +++ b/controllers/fetchData.js @@ -47,23 +47,9 @@ const fetchData = async () => { if (JSON.stringify(previousState) !== JSON.stringify(containerStatus)) { fs.writeFileSync(filePath, JSON.stringify(containerStatus, null, 2)); logger.info(`Container states saved to ${filePath}`); - - //TODO: rewrite every notification service using custom js modules - exec( - path.resolve(__dirname, "../misc/apprise.ppy"), - (error, stdout, stderr) => { - if (error) { - logger.error("Error executing apprise.py:", error.message); - return; - } - if (stderr) { - logger.warn("apprise.py stderr:", stderr); - } - logger.info("apprise.py executed successfully:", stdout); - }, - ); + //TODO: logic + notification levels per service } else { - logger.info("No state change detected, apprise.py not triggered."); + logger.info("No state change detected, notifications not triggered."); } } catch (error) { logger.error("Error fetching data:", error.message); diff --git a/controllers/frontendConfiguration.js b/controllers/frontendConfiguration.js index 2ba90e8c..cdbee131 100644 --- a/controllers/frontendConfiguration.js +++ b/controllers/frontendConfiguration.js @@ -2,9 +2,8 @@ const fs = require("fs"); const path = require("path"); const dataPath = path.join(__dirname, "../data/frontendConfiguration.json"); const logger = require("../utils/logger"); -const { PythonShellErrorWithLogs } = require("python-shell"); const expression = - "https?://(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)"; + "https?://(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}([-a-zA-Z0-9()@:%_+.~#?&//=]*)"; const regex = new RegExp(expression); /////////////////////////////////////////////////////////////// diff --git a/controllers/scheduler.js b/controllers/scheduler.js index 6322d327..e19b17eb 100644 --- a/controllers/scheduler.js +++ b/controllers/scheduler.js @@ -1,12 +1,10 @@ -// path: controllers/scheduler.js - const fetchData = require("./fetchData"); const logger = require("../utils/logger"); const db = require("../config/db"); +const regex = /(\d{1,5})([smh])/g; let fetchInterval = 5 * 60 * 1000; // Fetch data every 5 minutes by default let intervalId; -let cleanupIntervalId; const scheduleFetch = () => { fetchData().then(() => { @@ -20,7 +18,6 @@ const scheduleFetch = () => { fetchData(); }, fetchInterval); - // Schedule cleanup every 24 hours (86400000 ms) cleanupIntervalId = setInterval( () => { cleanupOldEntries(); @@ -50,7 +47,6 @@ const parseInterval = (interval) => { }; let totalMilliseconds = 0; - const regex = /(\d+)([smh])/g; let match; while ((match = regex.exec(interval))) { diff --git a/data/database.db b/data/database.db index d36f65d65fdc3cfd4736d30e65395b6a212c3393..400a0e0e09b55b0717a56b00a295397aa0b270ba 100644 GIT binary patch delta 19067 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 JMCiqq{|}7$b8P?s diff --git a/server.js b/server.js index a62b625d..35350977 100644 --- a/server.js +++ b/server.js @@ -30,12 +30,12 @@ swaggerDocs(app); scheduleFetch(); // Routes -app.use("/api", authMiddleware, limiter, api); -app.use("/conf", authMiddleware, limiter, conf); -app.use("/auth", authMiddleware, limiter, auth); -app.use("/data", authMiddleware, limiter, data); -app.use("/frontend", authMiddleware, limiter, frontend); -app.use("/notification-service", authMiddleware, limiter, notificationService); +app.use("/api", limiter, authMiddleware, api); +app.use("/conf", limiter, authMiddleware, conf); +app.use("/auth", limiter, authMiddleware, auth); +app.use("/data", limiter, authMiddleware, data); +app.use("/frontend", limiter, authMiddleware, frontend); +app.use("/notification-service", limiter, authMiddleware, notificationService); app.listen(PORT, () => { logger.info(`Server is running on http://localhost:${PORT}`); From 367bda08be3683126fbcca6a24d0e18e6e096d74 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 6 Nov 2024 01:34:47 +0100 Subject: [PATCH 014/369] Update mermaid diagramm for documentation purposes --- data/database.db | Bin 610304 -> 610304 bytes misc/dependencyGraphs/mermaid-all.txt | 124 +++++++++++------- misc/dependencyGraphs/mermaid-api.txt | 20 +-- misc/dependencyGraphs/mermaid-conf.txt | 16 ++- .../mermaid-notificationService.txt | 37 ++++++ 5 files changed, 137 insertions(+), 60 deletions(-) create mode 100644 misc/dependencyGraphs/mermaid-notificationService.txt diff --git a/data/database.db b/data/database.db index 400a0e0e09b55b0717a56b00a295397aa0b270ba..d916b79f6501db2292480baf31d83d3c2cb38163 100644 GIT binary patch delta 3699 zcmZ`+OKepc!ZIjqDAxWLfc|TI1%>wa~NFvfM2x{y_!~zn+S7)OEupd?MTtT(pt6WJNGWQukw_PG(Xax6M1o3{MNs+99gkKm_J-uBgU@X9%Ig9ORV=s0Fycxm8nfKk9GVuTn` zbKq`#|Kp7(j-5Gr<~tMnC{>0ED+G5}ZxzQP!-VASU?mZycN0$0##gap`7m1(VH-vuQDyVtn_!* zxI>52O>qu!H(yC^7P+ME^*_eriQ-&yPTkB$g~^Bg?L-UM(b-hs;96=X+M1(nmE)sSl?n(ip}iv+LX~h_c$vJX@G-PCu6n1;co>|I6~L zzqn_9n&^i(5iqMe5Q;D=g}}VKvnLoSArh@MjjN|dN^59i@j2x_^?vgGM)|S9kV{3} z$s=KHTvMcqAjXJJ7`R{*v1*)rvx4nbaW^|0jijrPcJ#wQy5~0(UW;)ct4w3&*~#Xo z2MfbN5s8Qp1e2R^uOyV(&dwSEhg+ti(YOV=rNLq)$)}lW%>3g~_xF#Za>VRDPJfk# z$d;cn@AT1nf8t3MrTH+Qm7 zuW3$<$^^Aq`fuw3ypCUSA*dpt+SrUSO_c(9;IJ-6b`$Ku#v}}COiC`41S3`cg|A~I z6#}_BdnBxGL(H_bLgVyZ)_`G332!eQ2uG6DMefe?;aETIfSzMjB?Z(a!&rUy98Pg6 zxNXp{O+Ml|MWuxsN#{|XS)BS5T2#eKX>EyHq6x^+27})gTf8eM)!27?TS#zAWBCEd z28sv?rgZkS#`Diucj!zq68Z&h@#W%(yOIWBvI@F8Un%bW$+u55-+nJngPO$YW#%ud zqu0fZKr@L84#!ou)t3HUH4L(qL4*)qo5liSTfe)2C^w*+t|G5=^ShdD7%Q?vyFabyP zgiRxk)Hd(+r@MlxMObkJn*GnTM$TdD#_YeUZu)Y3zl!3jA!a;N8g>yc4P6XHenJM^ z%#(3-Gs4SlcWFStQSe%p#Vm*O$`Iv%qeE%tmN$fR1uNXmcaus1YCVme#!(q9VbIG? z3UNgVDm`;crC=m&FK~+~{#(Q7fzq#QAh&^>WQyR4_||)1$cDAh>H1(WxrY*#cMOJM z5n7Hpj6T|1MU+G#fTy2Kt8U?MVK`1P0pfLz1_waJlrP&}Q%cxX=ZoA@R;=`Q!B7=+ zB6kaOgOWi$G70TNNmb3KX5+_#GD8w^DznvD(_B22ekQ2F4CK8wnIuAKW+44t5kN4l zl_+ya?p*V@FEc&ogRDU=kz|e8H}hJi^EIo-TSI;Mx#h@L9nUs3fjy> zB7y=I)P!GCcs*Z`*WflJn)JPRF(`x9R%F1rKQGbEJNKihU|12nGJk2kXmhB)@VTHY z4SdOLhT-361l~%_R!c2XvrEIlOsKInq_Kazl4=TV!V0&vJA|OOF<1X)+-U<6F13)_ zwlxg9cMQrk%$M@qb2zha6HvTcY3yYTARH%~P_d<;z=eBlEPcp*>1eul)XMHCK((kw z_vv?vuoEiU)u=G)b<59V6X@2_ba)U|pKU;12g&um1HGhg2=fXAxoY8!mCcnX2i*MK$cvRLV--k< z!&npu=s@$%g~SG7E<`)NobC(INUX_B4eb^(G()GM&=$7{)|KVxhfrW${hfH{m_*Cc zRTt1SK$^JQ|ND3(X6Qu~0)+k~X#@9;CznA=92Grc<}RK_oqFp;vS$l=WvF&{cofla zgYZ1@8k6IBa7ug?(r(9()8?2$#V{hXL@I^;7peyjpSy7|jmzP3U$P80{|;`h)<@vx zTF1>>Q3ws%Y2;X!b!d#xo=@M7e1T9#qd=kTwpjp=qbixaKqRX;JP}k-A3Wj~uO!-$ z-Ki@>tZ0T--?u*KJ$TFK*S0_Q*|xWv5cN{vUU(Bg2J+x%lts4p7=s@`Cbz}QI6>HYWt>2Xp!aPV&llt_PT*pX cIdY)%n+ooPq~f<4H4t(o;ocYDIGKL%Um?K6zyJUM delta 3213 zcmZ8jYlvM}8J%+D_XYSlbN@gaRSK@2txZjU4BJ(2@5j8<7QjsLkplB0kY?4-t zb|x0vLe(@E*;4;BAU+_WDXF;y1#|xy(`W|bFa6Q^C8A)W5lXGrwa+;-_e>eiIm21s zJ$vu9*IM6ReR<#N%lpoc@AN}MLqEhdh3nLdA25-B_2^GVk_SgF;l2*e z?`k^w_{i8}Pvh;8Z{zmpw{Uyp?9kOqdp|oGT{uUt(TH5HKY_Q;JiFs=gsXz9imQgJ z-rI3Ee(T+_nG?rP969~Ou`eB;S=5R$YCXGE-M5EwB?Y%m+xeN3$4?zO_0-HFV_Gq1 ztx*1=t~MWFTSb23%cM0tfYKoYb#Nu>@nw1zQ> zyu|naUGKyN6#crY{OIT6a}6hy)jD-n2*Vv`#(%0)HM^zeR!gVo;KR;1%c%1G%cMmL z-1k}1N(#jKd$;2G3s0Wx{mLXs)R2L`)F^>Lm1W%aHhx4Fq6QPx$`ph#3?R5+y}fV8 z2crhp&KPUc{Gpxor+=K-ae>XTh7TqJW)v3=<=vIb&BQRCmH73`N$Y_D=%YW!H>(ar z9PL!+CJYADTxwVPl8_vlQo{HDMA~tV`~EMnq%9;c)%)gu*Hhiqcg|Ee3me7CYXqNrvv<=KNP7S!xOs+cKmSEu$Z@$CN)l3F0?k#E`fHH@c8!66HuEl1sI$f|-HgMoWa>4>UreyR z0roRElWGW z_Rr#{XM-LsY@Ql8T{si$QQB-qrR}ax?%OR8e9GBvag7;krv0lwk6WaGLr}Nt#rxmZ zouB$d+-)Eu1Vbod zcxctVvPbVm%t?WCDp9K(xWN{!EYzDMLL-2r1ZT1qs*v<-^8|23P^m(#>962lCgu&p!_L3Qa-N5jzVc0&e@k=jM+sP)_ zUHhZ*%jaurqevlXQ_!2`DicK;gFY#y#xWX3K+G3yj62GWW)21o4~oPoZy6I(AZ@$r zXG0rv=yf2yG&`gL5(OKY=?>wW7j>`vzMc260$urJ{ITxxoeMPxn)$UG^|{Glc&Ec^ zODPvDEt%u~@QGS$c&j99jG5m9$UVCsWrqLGoVDn_Iz>y+2HCKE`qJQY!gX)I6GA{h ze^~fn_RdnyIpx_20X-}$@Jo+ZTcm*e?N3)0wp!RIe$H6Rw6G3J*RHqmR=g06ffK1= zc1vMz>@Qkd*1~4Ev<7*LUvMN)#$1V9Bb4^{4prv%pl3-c3_LfOT!;oM!4S_p8TwjQ z}e^Z z;D18}!T?E`H8zdZ{9vsWf*}Ci=xiPg4>|8_ zyulHS&((Bq=U2!})ec;sN*)WTU^vTqOpO14`g^;gW)>F#z(MBH7!x23QpYh&TO^7s z@kpAW80&#C`4kvC zSYixP1J6dJd8)qWq)9_}DLkOF#YhN}Cj@KirDj*hpwr`t|3M zTmId@LzzvKkA5xr T-uskb62}QJ_uZc^CvW~A!)8p4 diff --git a/misc/dependencyGraphs/mermaid-all.txt b/misc/dependencyGraphs/mermaid-all.txt index 0fc2d66d..6036bdd7 100644 --- a/misc/dependencyGraphs/mermaid-all.txt +++ b/misc/dependencyGraphs/mermaid-all.txt @@ -3,68 +3,104 @@ flowchart LR subgraph 0["config"] 1["db.js"] 2["swaggerConfig.js"] -9["dockerConfig.json"] +B["dockerConfig.json"] end subgraph 3["controllers"] 4["containerController.js"] -7["fetchData.js"] -A["frontendConfiguration.js"] -B["scheduler.js"] +7["databaseMigration.js"] +8["fetchData.js"] +C["frontendConfiguration.js"] +D["scheduler.js"] end subgraph 5["utils"] 6["dockerClient.js"] -8["containerService.js"] -O["extractHostData.js"] -P["writeOfflineLog.js"] +A["containerService.js"] +Q["extractHostData.js"] +R["writeOfflineLog.js"] +subgraph U["notifications"] +V["_notify.js"] +W["discord.js"] +subgraph X["data"] +Y["template.js"] end -subgraph C["middleware"] -D["authMiddleware.js"] -E["rateLimiter.js"] +Z["email.js"] +10["pushbullet.js"] +11["pushover.js"] +12["slack.js"] +13["telegram.js"] +14["whatsapp.js"] end -subgraph F["routes"] -subgraph G["auth"] -H["routes.js"] end -subgraph I["data"] +9["child_process"] +subgraph E["middleware"] +F["authMiddleware.js"] +G["rateLimiter.js"] +end +subgraph H["routes"] +subgraph I["auth"] J["routes.js"] end -subgraph K["frontendController"] +subgraph K["data"] L["routes.js"] end -subgraph M["getter"] +subgraph M["frontendController"] N["routes.js"] end -subgraph Q["setter"] -R["routes.js"] +subgraph O["getter"] +P["routes.js"] +end +subgraph S["notifications"] +T["routes.js"] +end +subgraph 15["setter"] +16["routes.js"] end end -S["server.js"] -subgraph T["swagger"] -U["swaggerDocs.js"] +17["server.js"] +subgraph 18["swagger"] +19["swaggerDocs.js"] end 4-->6 7-->1 -7-->8 +8-->1 +8-->A 8-->9 -8-->6 -B-->1 -B-->7 -J-->1 -L-->A -N-->9 -N-->B -N-->8 -N-->6 -N-->O -N-->P -R-->B -S-->B -S-->D -S-->E -S-->H -S-->J -S-->L -S-->N -S-->R -S-->U -U-->2 +A-->B +A-->6 +D-->1 +D-->8 +L-->1 +N-->C +P-->B +P-->D +P-->A +P-->6 +P-->Q +P-->R +T-->V +V-->W +V-->Z +V-->10 +V-->11 +V-->12 +V-->13 +V-->14 +W-->Y +Z-->Y +10-->Y +11-->Y +12-->Y +13-->Y +14-->Y +16-->D +17-->D +17-->F +17-->G +17-->J +17-->L +17-->N +17-->P +17-->T +17-->16 +17-->19 +19-->2 diff --git a/misc/dependencyGraphs/mermaid-api.txt b/misc/dependencyGraphs/mermaid-api.txt index c2dd6c86..0ae832b1 100644 --- a/misc/dependencyGraphs/mermaid-api.txt +++ b/misc/dependencyGraphs/mermaid-api.txt @@ -13,21 +13,23 @@ subgraph 5["controllers"] 6["scheduler.js"] 8["fetchData.js"] end -subgraph 9["utils"] -A["containerService.js"] -B["dockerClient.js"] -C["extractHostData.js"] -D["writeOfflineLog.js"] +9["child_process"] +subgraph A["utils"] +B["containerService.js"] +C["dockerClient.js"] +D["extractHostData.js"] +E["writeOfflineLog.js"] end 2-->4 2-->6 -2-->A 2-->B 2-->C 2-->D +2-->E 6-->7 6-->8 8-->7 -8-->A -A-->4 -A-->B +8-->B +8-->9 +B-->4 +B-->C diff --git a/misc/dependencyGraphs/mermaid-conf.txt b/misc/dependencyGraphs/mermaid-conf.txt index 65e4b74a..6e06cc6a 100644 --- a/misc/dependencyGraphs/mermaid-conf.txt +++ b/misc/dependencyGraphs/mermaid-conf.txt @@ -11,16 +11,18 @@ subgraph 3["controllers"] end subgraph 5["config"] 6["db.js"] -A["dockerConfig.json"] +B["dockerConfig.json"] end -subgraph 8["utils"] -9["containerService.js"] -B["dockerClient.js"] +8["child_process"] +subgraph 9["utils"] +A["containerService.js"] +C["dockerClient.js"] end 2-->4 4-->6 4-->7 7-->6 -7-->9 -9-->A -9-->B +7-->A +7-->8 +A-->B +A-->C diff --git a/misc/dependencyGraphs/mermaid-notificationService.txt b/misc/dependencyGraphs/mermaid-notificationService.txt new file mode 100644 index 00000000..dbfbd46c --- /dev/null +++ b/misc/dependencyGraphs/mermaid-notificationService.txt @@ -0,0 +1,37 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["notifications"] +2["routes.js"] +end +end +subgraph 3["utils"] +subgraph 4["notifications"] +5["_notify.js"] +6["discord.js"] +subgraph 7["data"] +8["template.js"] +end +9["email.js"] +A["pushbullet.js"] +B["pushover.js"] +C["slack.js"] +D["telegram.js"] +E["whatsapp.js"] +end +end +2-->5 +5-->6 +5-->9 +5-->A +5-->B +5-->C +5-->D +5-->E +6-->8 +9-->8 +A-->8 +B-->8 +C-->8 +D-->8 +E-->8 From 2d3af90ece7948ce7442675b4eecb5a3f4fa259c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 6 Nov 2024 01:37:52 +0100 Subject: [PATCH 015/369] Default route rewrite --- data/database.db | Bin 610304 -> 610304 bytes server.js | 5 +++++ 2 files changed, 5 insertions(+) diff --git a/data/database.db b/data/database.db index d916b79f6501db2292480baf31d83d3c2cb38163..d9d2a74b9e61093b16a7f252a33a5461125c2bf9 100644 GIT binary patch delta 1475 zcmY*ZO=w(I6wZ4y^WK|~G%uN#K@-ztCJBr&dEEb>sL-8I#8wog?V=qqxUkhxQz|Mt zX+^h*uX>;bi6H7mFw{0s3T75A{3&GIy3p*Dx=5u;k@|DyrO!+j=gyh?op;YY-}%mM zu1+^srcEk~;6c|iSaIAMv@Qq3uzb_s7-WV}YvRt5nt=N6 z{zI;&&>r$?j!w^qi6{Klv?M|?78^}d$IezyEu1=j`jz@~3)Oi_rDej}q=TyW-Mf}R>@c#@UV`+Eulu!Zf&w~g(!;|`N5)PQ)+z{o`3L0~U-RImIDPZfiZ*rV- zSPGo6yhblsd%2VxKJwLomNJ%vGKOuznqgujn9o~1TTND8i=c7Ti=gsFAxbX%^?fM0 zR$9b@8il=;6i)w`m;13t*lNLS7wuRPL*)^|Q!aum23p75sJ9Efc`W#@AO*1&+IRd~ znNn?bi{*k*WSqg)j8iIVN|_QE0hLCf6l7rX(NPJn|LuS63JLT>Zw6X%E`sJh_u3%# z8+90W6qyQA!^7i&ElX)7mY8v>wct|GG>eB>5QddGC-UzCnBf7q@oyn4a52y6m7jfY zV%=-zp7H(urE;NX>kQN%$(LdCQ}3-wrnyl@#mGwPpgy_;M2X^f|3Cbx5sp$K7~FLc zs+BNE#oA}yu(yLk{e~aqJNvNQ2(Uljcz}&S<-`Q1lH&oK0v14*whs=3HqLI(omWWYWGTN*be^@Yuq9oU3W{c`e^am6q03- zwa%I@a!(CHa4HngIdAe_rciB4U_eHu<=7=lQjK~$aM}<*1Lz@d7B>6y&5Do&r`5_B z)8LQUOO@gV=twXNdgMv^ZuTDZ-inty68f(=|UH6z#t+SQce9q zG-(BYK(K8nN3u#G=%!jrpl?yd&QfgAY`V2wiZ0rXh`8`flSwAvh4UWV@4S1y`8+6@9q9q3kJg9lQ_*|dqWW;!Gcpb_cO~9p>}gHy-?1E2GBDn7~pcf zvYLAEM)=%XDZqQLP4UV+4^gTOc z;rc0fC5akC`)EKCjo9r%Xm}( zkN?0~nXsPJ#rq%df1}JSRVRhVYfU!Bb~o|d8?23g{K(U)oYm^a zJz zSbugR;PRjBuTc^Ql+KN#J}WT72K?d<4^2g_7%-6oqse34Z}{WY9@Mi>gYWKoe0Yal z9hEW&@+mnaI2k~J#BV#?kCB^ba1J!pHfR}e?U|&>Jxu|@*!?}ul3l>t^_{zIJpBv% cyh)=?bK-Jgi5%UzP5+*?kL$@-_1g3Q0XXzQj{pDw diff --git a/server.js b/server.js index 35350977..1c03ae99 100644 --- a/server.js +++ b/server.js @@ -37,6 +37,11 @@ app.use("/data", limiter, authMiddleware, data); app.use("/frontend", limiter, authMiddleware, frontend); app.use("/notification-service", limiter, authMiddleware, notificationService); +// Default route +router.get("/", (req, res) => { + res.redirect("/api-docs"); +}); + app.listen(PORT, () => { logger.info(`Server is running on http://localhost:${PORT}`); logger.info(`Swagger docs available at http://localhost:${PORT}/api-docs`); From 74e7af2ed33d9f19b0d80501dbdc77612ad835d9 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 6 Nov 2024 01:44:53 +0100 Subject: [PATCH 016/369] Hot-Fix missing import --- data/database.db | Bin 610304 -> 610304 bytes server.js | 1 + 2 files changed, 1 insertion(+) diff --git a/data/database.db b/data/database.db index d9d2a74b9e61093b16a7f252a33a5461125c2bf9..fdb4b87da60d1775b970169b9ed2cc8b0bcd51d4 100644 GIT binary patch delta 1315 zcmZ8hO=w(I6wZC~=G~W&ArogZp(Ztx`6)%GFZcf^6f_$nxY0$skU~0(AX-|mwneI7 zrid=Y(C{>;i!KsSvvN@iorQ|Y#+@(~L9|flx(HLSASmc*W~RxwI5WKa9qu{j`@VD6 zw(4tJ_3Jaan|@?kojuaouLB7bHg!tYkAbo=wU zb9n;S*d0dbZuTuaz52;`mtX`K8H^Ak+aB*mfBo>xh1E+dm)@%^a&Ej5ib)CECQ}&W zOyIX+LPGN)Ez&-mF1=WQ*I4GOvbBo)L~7-vv%FC`zj}V<^1ID9S1XHx3&-H~5~;Ue zx=S-;UI{O?O{{m^XfEAQHkVAo-nsC2`?H^EKx$lQAthspx6D~-of%H&nt9l}Nb7T! zD`th)R>(%>=^)-n#hkPTmUvJYeVPR2Kj}0bU>hpxWQEX9r_GbqqeggcoY4$>8{DKk zlU#bIfscnp+E0PbdN36au+7tAf1d2wIZ1F*824BKUEn0jXth9DYQ(qbDA5gj2c!GDe&B<{Ed2h`FENlM8eJj zc@8e#qc6+|!JX3jV3H?}Va9T!_Gdx*WiJPo8ew7V08Ss>`*u=Gg%ZfbB4HX0W#IeS z_+*KDsSd8h9BwVj@Whz0Hk9-6~*cjMNGE-6SINf;EfQSEP^I4@9rMk0AqjEdi z%3P)Na%(y}@?aU7%V7yRJJDKA3xlHiL}PCTIsBh#nKe$P&C}IqrQ_Bp<I41 z8Jc<&HSi3j98~er<66M2BwGvxqUqWnCE7bwFmK}qbaG*z`3+!vnq6JYNGDMmdAnx}%o<^tI{hR*=*rATlM@DG1+ hfe$?(MmgrCfo3UFojiDnY@>p&*#{qY>DS%(?0*(8VFmyI delta 1163 zcmY*ZO=w(Y6rDGjdEc{4%}nQ|L8ED!WI!Z7zI*TYI~AcDMclL?F09aI5nQy1peb1V zX`-OGYnaw6c2hxgrKUtOf>4{CprBdUb+QxOOhCkipx0!kojf=^xaYk4op&s0gavS}*@PcrK4%#L0Dsh zCbHcVZT0N`^VX4|h#|^G43Xi5Q(2h62sYXw7fyo-hN5?Tc3F+3xUXQC8_AChammx_ z=Mg!I=Qop?$2W@+RO$D-dd{Ws?YJ-nbdXPdPn_}j5$vd#X-uv zC#SVxxT5kDbP$%eKsD0?g13K9=lG9*(|VP3+uF^$DxIp_DXBXfjj3;yTKCJ<(#{J! z|6Jub|N3QmwFM5O4Hc3eS&oYs4y{H)NQB(jF3&tl&Re7qC3yoo;I+&%Fw`nd)#v%- zdR4sbGkh&qZJv5Q?KEV1lIOx;i0EC=p}YH$x-oK$0(xIqfGC?oG{N%HqH5<6hrcc_ zi2~x?@$y)eJVfNJbMlcLlzr?Z&wN!!>1}-h`!_JAvp(q_amG<3I}~2BvS Date: Fri, 20 Dec 2024 22:03:41 +0100 Subject: [PATCH 017/369] Switch to ES6 and TypeScript (#21) * Full ES6 support * Full ES6 support * TODO: fix npm run dev in ts * TODO: fix 'ERROR : Error fetching data: ' * Delete files * Adding more typing; making code more logical at some points; * Added typing and fixed > dockstatapi@2 dep > bash ./src/utils/createDependencyGraph.sh Route: frontend Route: auth ./routes/frontendController/routes.ts ./routes/auth/routes.ts Route: data ./routes/data/routes.ts Route: notificationService ./routes/notifications/routes.ts Route: api ./routes/getter/routes.ts Route: conf ./routes/setter/routes.ts ======== DONE ======== * Added typing and fixed 'npm run dep' * Advanced logging and fixing some bugs * Adjust workflows * New README and some other docer adjustments * First time building (god damn) * Fixing some typings * Fixing some typings in default notification modules * Needs fixing! * Needs fixing! * Create CodeQL.yml * Fixing more errors * Added some more typings and better /api/status route * New mermaid deiagrams * New mermaid deiagrams * HA route * No building errors! * Remove CodeQL * Use selfhosted runners * nerver mind, using too much ressources on my cloud machine * Added HA functionality (Please test) * Fixing package versions * Fix: Creating default config if it doesn't exist * Fix: Sync endpoint * Fix: Endpoint reachability check * Chore: Update notification service * CI: Added cloc * Fix: adjust cloc workflox * Fix: exclude node modules from cloc * Fix: we have no yaml files, probably due to some other actions or smth * Fix: exclude package-lock from cloc * Created new dependency graphs * Feat: Added playwright tests for API endpoint + Swagger test (auth) (#23) Co-authored-by: ItsNik * Chore: Cleanup * Fix: Added proxy support * Chore: Custom notifications * Feat: minified build Chore: Alpine based Dockerfile Chore: Advance Lifecycle scripts included in dockstatapi@2: start tsx src/server.ts available via `npm run-script`: start:build npx tsc && export NODE_NO_WARNINGS=1 && node dist/server.js dev nodemon dev:trace nodemon --trace-uncaught --trace-warnings dep bash ./src/utils/createDependencyGraph.sh dep:remove bash ./src/utils/removeUnusedDeps.sh && bash ./src/utils/createDependencyGraph.sh build npx tsc build:mini npx tsc && bash ./src/misc/minifyDist.sh --build-only mini bash ./src/misc/minifyDist.sh scripts * Feat: minified build Chore: Alpine based Dockerfile Chore: Advance Lifecycle scripts included in dockstatapi@2: start tsx src/server.ts available via `npm run-script`: start:build npx tsc && export NODE_NO_WARNINGS=1 && node dist/server.js dev nodemon dev:trace nodemon --trace-uncaught --trace-warnings dep bash ./src/utils/createDependencyGraph.sh dep:remove bash ./src/utils/removeUnusedDeps.sh && bash ./src/utils/createDependencyGraph.sh build npx tsc build:mini npx tsc && bash ./src/misc/minifyDist.sh --build-only mini bash ./src/misc/minifyDist.sh scripts * Better docker image * Chore: Update docker image to alpine base * Fix: use find instead of tree in minifyDist.sh * Chore: Update Readme * Fix: Force correct node version (took some time to find it) * Fix: Typo in package.json * Feat: added npmc * Fix: Remove data-bak * Feat: Switch to yarn in Dockerfile * Feat: Switch to yarn in Dockerfile * Fix: Yarn does not work => Change to npm * Fix: Specify Node version using nvmrc in workflow file * Test: Try npm i --verbose to see why workflow times out * Test: Try with all environment files * Warn: Removing arm/v7 support due to docker build incompatibilities * Chore: Add test build for debugging with dockstatapi: * Chore: Add opencontainer labels * Chore: Added automatic notifications Chore: Add init.ts instead of one big server.ts Fix: Allow usePassword.txt in ./src/data (previously not included) * Fix: Adjusted .gitignore * Chore: Changed from ? true : false to simpler syntax * Chore: Added lock file when a sync is running * Chore: Remove any typing highAvailability.ts Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Chore: Update src/utils/connectionChecker.ts Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Fix: Missing Typing for HA * Fix: Environment Varaible usage inside docker * Fix: Forgot the copying of the file inside the Dockerfile (bruh) --------- Co-authored-by: ItsNik Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- .dockerignore | 2 + .github/DockStat.png | Bin 0 -> 79885 bytes .github/workflows/anchore.yml | 36 +- .github/workflows/build-dev.yaml | 2 +- .github/workflows/build-image.yml | 2 +- .github/workflows/cloc.yaml | 28 + .github/workflows/test-build.yaml | 59 + .gitignore | 15 +- .npmrc | 1 + .nvmrc | 1 + Dockerfile | 93 +- README.md | 36 +- TODO.md | 12 + config/db.js | 19 - config/dockerConfig.json | 9 - config/loggerConfig.js | 18 - config/swaggerConfig.js | 29 - controllers/fetchData.js | 59 - data/database.db | Bin 610304 -> 0 bytes entrypoint.sh | 26 - environment.d.ts | 44 + middleware/authMiddleware.js | 50 - middleware/password.json | 1 - misc/dependencyGraphs/mermaid-all.txt | 106 - misc/dependencyGraphs/mermaid-api.txt | 35 - misc/dependencyGraphs/mermaid-conf.txt | 28 - .../mermaid-notificationService.txt | 37 - misc/entrypoint.sh | 26 - nodemon.json | 6 + package-lock.json | 3449 +++++++++++++---- package.json | 55 +- playwright.config.ts | 37 + routes/auth/routes.js | 146 - routes/data/routes.js | 111 - routes/setter/routes.js | 145 - server.js | 49 - .../.dependency-cruiser.cjs | 255 +- src/config/db.ts | 30 + src/config/hostsystem.ts | 61 + src/config/loggerConfig.ts | 45 + src/config/swaggerConfig.ts | 53 + .../controllers/containerController.ts | 27 +- .../controllers/databaseMigration.ts | 12 +- src/controllers/fetchData.ts | 80 + .../controllers/frontendConfiguration.ts | 91 +- src/controllers/highAvailability.ts | 274 ++ src/controllers/notificationController.ts | 62 + src/controllers/proxy.ts | 14 + .../controllers/scheduler.ts | 53 +- {middleware => src/data}/usePassword.txt | 0 src/init.ts | 47 + src/middleware/authMiddleware.ts | 52 + src/middleware/checkLock.ts | 19 + .../middleware/rateLimiter.ts | 0 src/misc/createEnvFile.sh | 34 + src/misc/dependencyGraphs/mermaid-all.txt | 106 + src/misc/dependencyGraphs/mermaid-api.txt | 32 + .../misc}/dependencyGraphs/mermaid-auth.txt | 2 +- src/misc/dependencyGraphs/mermaid-conf.txt | 24 + .../misc}/dependencyGraphs/mermaid-data.txt | 4 +- .../dependencyGraphs/mermaid-frontend.txt | 4 +- src/misc/dependencyGraphs/mermaid-ha.txt | 11 + .../mermaid-notificationService.txt | 35 + src/misc/entrypoint.sh | 30 + src/misc/minifyDist.sh | 38 + src/routes/auth/routes.ts | 174 + src/routes/data/routes.ts | 201 + .../routes/frontendController/routes.ts | 33 +- .../routes.js => src/routes/getter/routes.ts | 160 +- src/routes/highavailability/routes.ts | 92 + .../routes/notifications/routes.ts | 76 +- src/routes/setter/routes.ts | 180 + src/server.ts | 17 + src/utils/connectionChecker.ts | 77 + src/utils/containerService.ts | 134 + src/utils/createDependencyGraph.sh | 37 + src/utils/dockerClient.ts | 54 + src/utils/extractHostData.ts | 57 + utils/logger.js => src/utils/logger.ts | 8 +- src/utils/notifications/_notify.ts | 85 + .../utils/notifications/_template.ts | 42 +- src/utils/notifications/discord.ts | 55 + src/utils/notifications/email.ts | 46 + src/utils/notifications/pushbullet.ts | 59 + src/utils/notifications/pushover.ts | 56 + src/utils/notifications/slack.ts | 55 + src/utils/notifications/telegram.ts | 55 + src/utils/notifications/whatsapp.ts | 57 + src/utils/removeUnusedDeps.sh | 36 + src/utils/swaggerDocs.ts | 11 + src/utils/writeOfflineLog.ts | 26 + swagger/swaggerDocs.js | 10 - tests/main.spec.ts | 131 + tsconfig.json | 21 + utils/containerService.js | 63 - utils/createDependencyGraph.sh | 34 - utils/dockerClient.js | 45 - utils/extractHostData.js | 26 - utils/notifications/_notify.js | 59 - utils/notifications/data/template.json | 3 - utils/notifications/discord.js | 27 - utils/notifications/email.js | 36 - utils/notifications/pushbullet.js | 30 - utils/notifications/pushover.js | 30 - utils/notifications/slack.js | 27 - utils/notifications/telegram.js | 32 - utils/notifications/whatsapp.js | 29 - utils/writeOfflineLog.js | 31 - yarn.lock | 3298 ++++++++++++++++ 109 files changed, 9584 insertions(+), 2498 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/DockStat.png create mode 100644 .github/workflows/cloc.yaml create mode 100644 .github/workflows/test-build.yaml create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 TODO.md delete mode 100644 config/db.js delete mode 100644 config/dockerConfig.json delete mode 100644 config/loggerConfig.js delete mode 100644 config/swaggerConfig.js delete mode 100644 controllers/fetchData.js delete mode 100644 data/database.db delete mode 100644 entrypoint.sh create mode 100644 environment.d.ts delete mode 100644 middleware/authMiddleware.js delete mode 100644 middleware/password.json delete mode 100644 misc/dependencyGraphs/mermaid-all.txt delete mode 100644 misc/dependencyGraphs/mermaid-api.txt delete mode 100644 misc/dependencyGraphs/mermaid-conf.txt delete mode 100644 misc/dependencyGraphs/mermaid-notificationService.txt delete mode 100755 misc/entrypoint.sh create mode 100644 nodemon.json create mode 100644 playwright.config.ts delete mode 100644 routes/auth/routes.js delete mode 100644 routes/data/routes.js delete mode 100644 routes/setter/routes.js delete mode 100644 server.js rename .dependency-cruiser.js => src/.dependency-cruiser.cjs (66%) create mode 100644 src/config/db.ts create mode 100644 src/config/hostsystem.ts create mode 100644 src/config/loggerConfig.ts create mode 100644 src/config/swaggerConfig.ts rename controllers/containerController.js => src/controllers/containerController.ts (58%) rename controllers/databaseMigration.js => src/controllers/databaseMigration.ts (55%) create mode 100644 src/controllers/fetchData.ts rename controllers/frontendConfiguration.js => src/controllers/frontendConfiguration.ts (73%) create mode 100644 src/controllers/highAvailability.ts create mode 100644 src/controllers/notificationController.ts create mode 100644 src/controllers/proxy.ts rename controllers/scheduler.js => src/controllers/scheduler.ts (56%) rename {middleware => src/data}/usePassword.txt (100%) create mode 100644 src/init.ts create mode 100644 src/middleware/authMiddleware.ts create mode 100644 src/middleware/checkLock.ts rename middleware/rateLimiter.js => src/middleware/rateLimiter.ts (100%) create mode 100644 src/misc/createEnvFile.sh create mode 100644 src/misc/dependencyGraphs/mermaid-all.txt create mode 100644 src/misc/dependencyGraphs/mermaid-api.txt rename {misc => src/misc}/dependencyGraphs/mermaid-auth.txt (80%) create mode 100644 src/misc/dependencyGraphs/mermaid-conf.txt rename {misc => src/misc}/dependencyGraphs/mermaid-data.txt (78%) rename {misc => src/misc}/dependencyGraphs/mermaid-frontend.txt (71%) create mode 100644 src/misc/dependencyGraphs/mermaid-ha.txt create mode 100644 src/misc/dependencyGraphs/mermaid-notificationService.txt create mode 100755 src/misc/entrypoint.sh create mode 100644 src/misc/minifyDist.sh create mode 100644 src/routes/auth/routes.ts create mode 100644 src/routes/data/routes.ts rename routes/frontendController/routes.js => src/routes/frontendController/routes.ts (97%) rename routes/getter/routes.js => src/routes/getter/routes.ts (68%) create mode 100644 src/routes/highavailability/routes.ts rename routes/notifications/routes.js => src/routes/notifications/routes.ts (73%) create mode 100644 src/routes/setter/routes.ts create mode 100644 src/server.ts create mode 100644 src/utils/connectionChecker.ts create mode 100644 src/utils/containerService.ts create mode 100755 src/utils/createDependencyGraph.sh create mode 100644 src/utils/dockerClient.ts create mode 100644 src/utils/extractHostData.ts rename utils/logger.js => src/utils/logger.ts (52%) create mode 100644 src/utils/notifications/_notify.ts rename utils/notifications/data/template.js => src/utils/notifications/_template.ts (51%) create mode 100644 src/utils/notifications/discord.ts create mode 100644 src/utils/notifications/email.ts create mode 100644 src/utils/notifications/pushbullet.ts create mode 100644 src/utils/notifications/pushover.ts create mode 100644 src/utils/notifications/slack.ts create mode 100644 src/utils/notifications/telegram.ts create mode 100644 src/utils/notifications/whatsapp.ts create mode 100755 src/utils/removeUnusedDeps.sh create mode 100644 src/utils/swaggerDocs.ts create mode 100644 src/utils/writeOfflineLog.ts delete mode 100644 swagger/swaggerDocs.js create mode 100644 tests/main.spec.ts create mode 100644 tsconfig.json delete mode 100644 utils/containerService.js delete mode 100755 utils/createDependencyGraph.sh delete mode 100644 utils/dockerClient.js delete mode 100644 utils/extractHostData.js delete mode 100644 utils/notifications/_notify.js delete mode 100644 utils/notifications/data/template.json delete mode 100644 utils/notifications/discord.js delete mode 100644 utils/notifications/email.js delete mode 100644 utils/notifications/pushbullet.js delete mode 100644 utils/notifications/pushover.js delete mode 100644 utils/notifications/slack.js delete mode 100644 utils/notifications/telegram.js delete mode 100644 utils/notifications/whatsapp.js delete mode 100644 utils/writeOfflineLog.js create mode 100644 yarn.lock diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..10b44aec --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +*.txt +*.md \ No newline at end of file diff --git a/.github/DockStat.png b/.github/DockStat.png new file mode 100644 index 0000000000000000000000000000000000000000..d375bd49107c79a960488d6062276a72cf6bd512 GIT binary patch literal 79885 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⪼EggaS+_T%ej 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 KU+70|um2a&GMD85 literal 0 HcmV?d00001 diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 053123d0..bb5df127 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -13,12 +13,12 @@ name: Anchore Grype vulnerability scan on: push: - branches: [ "main" ] + branches: ["main"] pull_request: # The branches below must be a subset of the branches above - branches: [ "main" ] + branches: ["main", "dev"] schedule: - - cron: '30 9 * * 1' + - cron: "30 9 * * 1" permissions: contents: read @@ -31,18 +31,18 @@ jobs: actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status runs-on: ubuntu-latest steps: - - name: Check out the code - uses: actions/checkout@v4 - - name: Build the Docker image - run: docker build . --file Dockerfile --tag localbuild/testimage:latest - - name: Run the Anchore Grype scan action - uses: anchore/scan-action@d5aa5b6cb9414b0c7771438046ff5bcfa2854ed7 - id: scan - with: - image: "localbuild/testimage:latest" - fail-build: true - severity-cutoff: critical - - name: Upload vulnerability report - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: ${{ steps.scan.outputs.sarif }} + - name: Check out the code + uses: actions/checkout@v4 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag localbuild/testimage:latest + - name: Run the Anchore Grype scan action + uses: anchore/scan-action@d5aa5b6cb9414b0c7771438046ff5bcfa2854ed7 + id: scan + with: + image: "localbuild/testimage:latest" + fail-build: true + severity-cutoff: critical + - name: Upload vulnerability report + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{ steps.scan.outputs.sarif }} diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index 9833ede7..afc8ffc1 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -1,4 +1,4 @@ -name: Docker Image CI (dev) +name: Build dockstatapi:nightly on: push: diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 8668f9ba..4097b0b0 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -1,4 +1,4 @@ -name: Docker Image CI +name: Buiod dockstatapi:latest on: release: diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml new file mode 100644 index 00000000..9ce7e271 --- /dev/null +++ b/.github/workflows/cloc.yaml @@ -0,0 +1,28 @@ +name: Count Lines of Code + +permissions: + issues: write + pull-requests: write + +on: + push: + branches: [ main ] + pull_request: + branches: [ main, dev ] + +jobs: + cloc: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Count Lines of Code (cloc) + uses: djdefi/cloc-action@6 + with: + options: --md --report-file=cloc.md --exclude-dir=node_modules --exclude-lang=YAML,JSON --exclude-list-file=package-lock.json + + - name: Create comment from markdown file + uses: GrantBirki/comment@v2.1.0 + with: + file: cloc.md \ No newline at end of file diff --git a/.github/workflows/test-build.yaml b/.github/workflows/test-build.yaml new file mode 100644 index 00000000..fb471834 --- /dev/null +++ b/.github/workflows/test-build.yaml @@ -0,0 +1,59 @@ +name: Test building + +on: + pull_request: + branches: + - "dev" + +permissions: + packages: write + contents: read + +jobs: + build-main: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Set up Node.js using nvm + - name: Set up Node.js version from .nvmrc + run: | + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + nvm install + nvm use + node -v + npm -v + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate Docker tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,enable=true,priority=200,prefix=,suffix=,value=${{ github.sha }} + + - name: Build and Push Docker Images + uses: docker/build-push-action@v5 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index f7fcc52b..43ddf882 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,13 @@ # custom paths: -data/* - +src/data/database.db +src/data/dockerConfig.json +src/data/highAvailability.json +src/data/states.json +src/data/user.conf +src/data/password.json +src/data/ha.lock + +.test* # Created by https://www.toptal.com/developers/gitignore/api/node ### Node ### # Logs @@ -141,3 +148,7 @@ dist # SvelteKit build / generate output .svelte-kit +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..4fd02195 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..209e3ef4 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/Dockerfile b/Dockerfile index b23d93c2..87792b05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,59 @@ -# Stage 1: Build stage -FROM node:latest AS builder - -LABEL maintainer="https://github.com/its4nik" -LABEL version="2" -LABEL description="API for DockStat" -LABEL license="BSD-3-Clause license " -LABEL repository="https://github.com/its4nik/dockstatapi" -LABEL documentation="https://github.com/its4nik/dockstatapi" - -WORKDIR /api - -COPY package*.json ./ - -RUN npm install --production - -COPY . . - -# Stage 2: Production stage -FROM node:alpine - -WORKDIR /api - -COPY --from=builder /api . - -RUN apk add --no-cache \ - bash \ - curl - -EXPOSE 7070 - -HEALTHCHECK CMD curl --fail http://localhost:7070/api/status || exit 1 - -ENTRYPOINT [ "bash", "misc/entrypoint.sh" ] +# Stage 1: Build stage +FROM node:alpine AS builder + +LABEL maintainer="https://github.com/its4nik" +LABEL version="2" +LABEL description="API for DockStat" +LABEL license="BSD-3-Clause license" +LABEL repository="https://github.com/its4nik/dockstatapi" +LABEL documentation="https://github.com/its4nik/dockstatapi" +LABEL org.opencontainers.image.description "The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" +LABEL org.opencontainers.image.licenses="BSD-3-Clause license" +LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" + +WORKDIR /build +ENV NODE_NO_WARNINGS=1 + +RUN apk update && \ + apk upgrade && \ + apk add bash + + +COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +RUN npm install --verbose + +COPY ./src ./src +RUN npm run build:mini + +# Stage 2: main stage +FROM alpine AS main + +# Needed packages +RUN apk update && \ + apk upgrade && \ + apk add --update npm + +WORKDIR /build + +RUN mkdir -p /build/src/data + +COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +RUN npm install --omit=dev --verbose + +COPY --from=builder /build/dist/* /build/src +COPY --from=builder /build/src/misc/entrypoint.sh /build/entrypoint.sh +COPY --from=builder /build/src/misc/createEnvFile.sh /build/createEnvFile.sh + +RUN node src/config/db.js + +# Stage 3: Production stage +FROM alpine AS production +ARG RUNNING_IN_DOCKER=true +RUN apk add --update bash nodejs + +WORKDIR /api + +COPY --from=main /build /api + +EXPOSE 9876 +ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/README.md b/README.md index c12afae4..ae34767f 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,45 @@ # DockStatAPI v2 +![Dockstat Logo](.github/DockStat.png) This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. -With this new release a cupple of extra features (compared to v1) are going to be available. +With this new release a couple of extra features (compared to v1) are going to be available. ### Feature List: - Swagger API Documentation -- "Offline" mode (useful when working on the backend without available test docker sockets) - Database (Keeps data for 24 hours max) - Advanced authentication using hashes and salt +- Custom TypeScript/JavaScript notification modules! (Easy to add and configure!) +- `http` API to configure the backend +- Multi-arch docker builds (using buildx github action) +- Advanced security through middlewares: rate-limiting and authentication +- Multi Arch Docker builds through docker buildx +- High Availability using single master and ulimited worker nodes! # 🔗 DockStatAPI v2 Documentation + +_⚠️ = Deprecation warning_ + +- [Introduction](https://outline.itsnik.de/s/dockstat) + + - [DockstatAPI v2](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v2-XRMDKRqMIg) + + - [API reference](https://outline.itsnik.de/s/dockstat/doc/api-reference-1PTxqx1MQ6) + - [How dependency graphs are made](https://outline.itsnik.de/s/dockstat/doc/how-the-dependecy-graphs-are-made-svuZbEHH9g) + + - [DockStat v1](https://outline.itsnik.de/s/dockstat/doc/dockstat-v1-zVaFS4zROI) + + - [⚠️ Customisation](https://outline.itsnik.de/s/dockstat/doc/customization-PiBz4OpQIZ) + - [⚠️ Themes](https://outline.itsnik.de/s/dockstat/doc/themes-BFhN6ZBbYx) + - [⚠️ Installation](https://outline.itsnik.de/s/dockstat/doc/installation-DaO99bB86q) + + - [⚠️ DockStatAPI v1](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v1-jLcVCfPNmS) + - [⚠️ Integrations](https://outline.itsnik.de/s/dockstat/doc/integrations-Agq1oL6HxF) + - [⚠️ Backend API reference](https://outline.itsnik.de/s/dockstat/doc/backend-api-reference-YzcBbDvY33) + +# DockStat(APIs) goals + +DockStack tries to be a lightweigh and more "dashboard" like then [portainer](https://github.com/portainer/portainer), [cAdvisor](https://github.com/google/cadvisor), [dockge](https://github.com/louislam/dockge), ... +I also try to add some "extensions", like in V1 with [🥤cup](https://github.com/sergi0g/cup). +Everything is configured through a backend with Swagger documentation, so that you can follow the code and understand the new v2 frontend better! +DockStat is mainly used for teaching [myself](https://github.com/Its4Nik) more about TypeScript, APIs and backend development! diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..d2659e2d --- /dev/null +++ b/TODO.md @@ -0,0 +1,12 @@ +- [ ] Better Offline mode using "faker" library or self written (probably self written) +- [X] HA compatibility +- [X] !!! Needs testing !!! Add automatic notifications when container state changes, according to selected level for notification service +- [ ] Image update and update notifications +- [ ] trigger container restart / stop / start via backend routes +- [X] Add more logging +- [X] Structure code differently +- [X] Write new README and make the docs better +- [X] Update more files to correct TS syntax => remove "any" +- [ ] Websockets +- [X] Better /api/status endpoint with connection status of each host +- [X] Update notification service diff --git a/config/db.js b/config/db.js deleted file mode 100644 index 51850d3e..00000000 --- a/config/db.js +++ /dev/null @@ -1,19 +0,0 @@ -const sqlite3 = require("sqlite3").verbose(); -const logger = require("./../utils/logger"); -const path = require("path"); -const dbPath = path.join(__dirname, "../data/database.db"); - -const db = new sqlite3.Database(dbPath, (err) => { - if (err) { - logger.error("Error opening database:", err.message); - } else { - db.run(`CREATE TABLE IF NOT EXISTS data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - info TEXT NOT NULL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP - )`); - logger.info("Database created / opened succesfully"); - } -}); - -module.exports = db; diff --git a/config/dockerConfig.json b/config/dockerConfig.json deleted file mode 100644 index 9ec4caf3..00000000 --- a/config/dockerConfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "hosts": [ - { - "name": "Fin-2", - "url": "100.89.35.135", - "port": "2375" - } - ] -} diff --git a/config/loggerConfig.js b/config/loggerConfig.js deleted file mode 100644 index 38149ec4..00000000 --- a/config/loggerConfig.js +++ /dev/null @@ -1,18 +0,0 @@ -const { createLogger, format, transports } = require("winston"); - -const logger = createLogger({ - level: "info", - format: format.combine( - format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), - format.printf( - ({ timestamp, level, message }) => - `[${timestamp}] ${level.toUpperCase()}: ${message}`, - ), - ), - transports: [ - new transports.Console(), - new transports.File({ filename: "logs/app.log" }), - ], -}); - -module.exports = logger; diff --git a/config/swaggerConfig.js b/config/swaggerConfig.js deleted file mode 100644 index 723897fc..00000000 --- a/config/swaggerConfig.js +++ /dev/null @@ -1,29 +0,0 @@ -const options = { - definition: { - failOnErrors: true, - openapi: "3.0.0", - info: { - title: "DockStatAPI", - version: "2", - description: "An API used to query muliple docker hosts", - }, - components: { - securitySchemes: { - passwordAuth: { - type: "apiKey", - in: "header", - name: "x-password", - description: "Password required for authentication", - }, - }, - }, - security: [ - { - passwordAuth: [], - }, - ], - }, - apis: ["./routes/*/*.js"], -}; - -module.exports = options; diff --git a/controllers/fetchData.js b/controllers/fetchData.js deleted file mode 100644 index ba14c348..00000000 --- a/controllers/fetchData.js +++ /dev/null @@ -1,59 +0,0 @@ -const db = require("../config/db"); -const { fetchAllContainers } = require("../utils/containerService"); -const logger = require("./../utils/logger"); -const path = require("path"); -const fs = require("fs"); -const { exec } = require("child_process"); - -const fetchData = async () => { - try { - const allContainerData = await fetchAllContainers(); - const data = allContainerData; - - if (process.env.OFFLINE === "true") { - logger.info("No new data inserted --- OFFLINE MODE"); - } else { - db.run( - `INSERT INTO data (info) VALUES (?)`, - [JSON.stringify(data)], - function (error) { - if (error) { - logger.info("Error inserting data:", error.message); - console.error("Error inserting data:", error.message); - return; - } - logger.info(`Data inserted with ID: ${this.lastID}`); - }, - ); - } - - const containerStatus = {}; - Object.keys(allContainerData).forEach((host) => { - containerStatus[host] = allContainerData[host].map((container) => ({ - name: container.name, - id: container.id, - state: container.state, - host: container.hostName, - })); - }); - - const filePath = path.resolve(__dirname, "../data/states.json"); - let previousState = {}; - - if (fs.existsSync(filePath)) { - previousState = JSON.parse(fs.readFileSync(filePath, "utf8")); - } - - if (JSON.stringify(previousState) !== JSON.stringify(containerStatus)) { - fs.writeFileSync(filePath, JSON.stringify(containerStatus, null, 2)); - logger.info(`Container states saved to ${filePath}`); - //TODO: logic + notification levels per service - } else { - logger.info("No state change detected, notifications not triggered."); - } - } catch (error) { - logger.error("Error fetching data:", error.message); - } -}; - -module.exports = fetchData; diff --git a/data/database.db b/data/database.db deleted file mode 100644 index fdb4b87da60d1775b970169b9ed2cc8b0bcd51d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 610304 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 T!%x$uzO8Xp=N2W3`G5X@A5s6M diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index 2008cdbd..00000000 --- a/entrypoint.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -cat << EOF -Welcome to - - ###### ###### #### ### ### #### ######### ###### ######### - ### ### ### ### ### ### ### ### ### ### ### ### - ### ### ### ### ### ###### #### ### ### ### ### - ### ### ### ### ### ### ### #### ### ############ ### - ### ### ### ### ### ### ### #### ### ### ### ### - ###### ###### #### ### ### #### ### ### ### ### (API) - -Useful links: - -- Documentation: https://outline.itsnik.de/s/dockstat -- GitHub (Frontend): https://github.com/its4nik/dockstat -- GitHub (Backend): https://github.com/its4nik/dockstatapi -- API Documentation: http://localhost:7000/api-docs - -Summary: - -DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. - -EOF - -npm run start diff --git a/environment.d.ts b/environment.d.ts new file mode 100644 index 00000000..803ae43c --- /dev/null +++ b/environment.d.ts @@ -0,0 +1,44 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + // Node specific: + NODE_ENV: "development" | "production"; + TRUSTED_PROXYS: string | undefined; + + // User.conf + RUNNING_IN_DOCKER: string | undefined; + VERSION: string | undefined; + + // High Availability + HA_MASTER: string | undefined; //bool + HA_MASTER_IP: string | undefined; + HA_NODE: string | undefined; //ip list with port seperated by "," like: "10.0.0.4:5012,10.0.0.5:9876" + HA_UNSAFE: string | undefined; + + // Notification services: + DISCORD_WEBHOOK_URL: string | undefined; + + EMAIL_SENDER: string | undefined; + EMAIL_RECIPIENT: string | undefined; + EMAIL_PASSWORD: string | undefined; + EMAIL_SERVICE: string | undefined; + + PUSHBULLET_ACCESS_TOKEN: string | undefined; + + PUSHOVER_USER_KEY: string | undefined; + PUSHOVER_API_TOKEN: string | undefined; + + SLACK_WEBHOOK_URL: string | undefined; + + TELEGRAM_BOT_TOKEN: string | undefined; + TELEGRAM_CHAT_ID: string | undefined; + + WHATSAPP_API_URL: string | undefined; + WHATSAPP_RECIPIENT: string | undefined; + + CUSTOM_NOTIFICATION: string | undefined; // enter the script name without .js here and without custom/... + } + } +} + +export {}; diff --git a/middleware/authMiddleware.js b/middleware/authMiddleware.js deleted file mode 100644 index 8ee6a688..00000000 --- a/middleware/authMiddleware.js +++ /dev/null @@ -1,50 +0,0 @@ -const bcrypt = require("bcrypt"); -const fs = require("fs"); -const path = require("path"); -const logger = require("../utils/logger"); -const passwordFile = path.join(__dirname, "password.json"); -const passwordBool = path.join(__dirname, "usePassword.txt"); - -function authMiddleware(req, res, next) { - fs.readFile(passwordBool, "utf8", (err, data) => { - if (err) { - logger.error("Error reading the file:", err); - return; - } - - const isAuthEnabled = data.trim() === "true"; - - if (!isAuthEnabled) { - return next(); - } - - const providedPassword = req.headers["x-password"]; - if (!providedPassword) { - logger.error("Password required - Denied"); - return res.status(401).json({ message: "Password required" }); - } - - fs.readFile(passwordFile, "utf8", (err, data) => { - if (err) { - logger.error("Error reading password"); - return res.status(500).json({ message: "Error reading password" }); - } - - const storedData = JSON.parse(data); - bcrypt.compare(providedPassword, storedData.hash, (err, result) => { - if (err) { - logger.error("Error validating password - Denied access"); - return res.status(500).json({ message: "Error validating password" }); - } - if (!result) { - console.error("Invalid Password - Denied access"); - return res.status(401).json({ message: "Invalid password" }); - } - - next(); - }); - }); - }); -} - -module.exports = authMiddleware; diff --git a/middleware/password.json b/middleware/password.json deleted file mode 100644 index 37a7c4c4..00000000 --- a/middleware/password.json +++ /dev/null @@ -1 +0,0 @@ -{"hash":"$2b$10$qGcNmciEGhX.PiB.ofHib.Fob.nOjQNfguBoD4JDbbbTysrLrKGEi","salt":"$2b$10$qGcNmciEGhX.PiB.ofHib."} \ No newline at end of file diff --git a/misc/dependencyGraphs/mermaid-all.txt b/misc/dependencyGraphs/mermaid-all.txt deleted file mode 100644 index 6036bdd7..00000000 --- a/misc/dependencyGraphs/mermaid-all.txt +++ /dev/null @@ -1,106 +0,0 @@ -flowchart LR - -subgraph 0["config"] -1["db.js"] -2["swaggerConfig.js"] -B["dockerConfig.json"] -end -subgraph 3["controllers"] -4["containerController.js"] -7["databaseMigration.js"] -8["fetchData.js"] -C["frontendConfiguration.js"] -D["scheduler.js"] -end -subgraph 5["utils"] -6["dockerClient.js"] -A["containerService.js"] -Q["extractHostData.js"] -R["writeOfflineLog.js"] -subgraph U["notifications"] -V["_notify.js"] -W["discord.js"] -subgraph X["data"] -Y["template.js"] -end -Z["email.js"] -10["pushbullet.js"] -11["pushover.js"] -12["slack.js"] -13["telegram.js"] -14["whatsapp.js"] -end -end -9["child_process"] -subgraph E["middleware"] -F["authMiddleware.js"] -G["rateLimiter.js"] -end -subgraph H["routes"] -subgraph I["auth"] -J["routes.js"] -end -subgraph K["data"] -L["routes.js"] -end -subgraph M["frontendController"] -N["routes.js"] -end -subgraph O["getter"] -P["routes.js"] -end -subgraph S["notifications"] -T["routes.js"] -end -subgraph 15["setter"] -16["routes.js"] -end -end -17["server.js"] -subgraph 18["swagger"] -19["swaggerDocs.js"] -end -4-->6 -7-->1 -8-->1 -8-->A -8-->9 -A-->B -A-->6 -D-->1 -D-->8 -L-->1 -N-->C -P-->B -P-->D -P-->A -P-->6 -P-->Q -P-->R -T-->V -V-->W -V-->Z -V-->10 -V-->11 -V-->12 -V-->13 -V-->14 -W-->Y -Z-->Y -10-->Y -11-->Y -12-->Y -13-->Y -14-->Y -16-->D -17-->D -17-->F -17-->G -17-->J -17-->L -17-->N -17-->P -17-->T -17-->16 -17-->19 -19-->2 diff --git a/misc/dependencyGraphs/mermaid-api.txt b/misc/dependencyGraphs/mermaid-api.txt deleted file mode 100644 index 0ae832b1..00000000 --- a/misc/dependencyGraphs/mermaid-api.txt +++ /dev/null @@ -1,35 +0,0 @@ -flowchart LR - -subgraph 0["routes"] -subgraph 1["getter"] -2["routes.js"] -end -end -subgraph 3["config"] -4["dockerConfig.json"] -7["db.js"] -end -subgraph 5["controllers"] -6["scheduler.js"] -8["fetchData.js"] -end -9["child_process"] -subgraph A["utils"] -B["containerService.js"] -C["dockerClient.js"] -D["extractHostData.js"] -E["writeOfflineLog.js"] -end -2-->4 -2-->6 -2-->B -2-->C -2-->D -2-->E -6-->7 -6-->8 -8-->7 -8-->B -8-->9 -B-->4 -B-->C diff --git a/misc/dependencyGraphs/mermaid-conf.txt b/misc/dependencyGraphs/mermaid-conf.txt deleted file mode 100644 index 6e06cc6a..00000000 --- a/misc/dependencyGraphs/mermaid-conf.txt +++ /dev/null @@ -1,28 +0,0 @@ -flowchart LR - -subgraph 0["routes"] -subgraph 1["setter"] -2["routes.js"] -end -end -subgraph 3["controllers"] -4["scheduler.js"] -7["fetchData.js"] -end -subgraph 5["config"] -6["db.js"] -B["dockerConfig.json"] -end -8["child_process"] -subgraph 9["utils"] -A["containerService.js"] -C["dockerClient.js"] -end -2-->4 -4-->6 -4-->7 -7-->6 -7-->A -7-->8 -A-->B -A-->C diff --git a/misc/dependencyGraphs/mermaid-notificationService.txt b/misc/dependencyGraphs/mermaid-notificationService.txt deleted file mode 100644 index dbfbd46c..00000000 --- a/misc/dependencyGraphs/mermaid-notificationService.txt +++ /dev/null @@ -1,37 +0,0 @@ -flowchart LR - -subgraph 0["routes"] -subgraph 1["notifications"] -2["routes.js"] -end -end -subgraph 3["utils"] -subgraph 4["notifications"] -5["_notify.js"] -6["discord.js"] -subgraph 7["data"] -8["template.js"] -end -9["email.js"] -A["pushbullet.js"] -B["pushover.js"] -C["slack.js"] -D["telegram.js"] -E["whatsapp.js"] -end -end -2-->5 -5-->6 -5-->9 -5-->A -5-->B -5-->C -5-->D -5-->E -6-->8 -9-->8 -A-->8 -B-->8 -C-->8 -D-->8 -E-->8 diff --git a/misc/entrypoint.sh b/misc/entrypoint.sh deleted file mode 100755 index 2008cdbd..00000000 --- a/misc/entrypoint.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -cat << EOF -Welcome to - - ###### ###### #### ### ### #### ######### ###### ######### - ### ### ### ### ### ### ### ### ### ### ### ### - ### ### ### ### ### ###### #### ### ### ### ### - ### ### ### ### ### ### ### #### ### ############ ### - ### ### ### ### ### ### ### #### ### ### ### ### - ###### ###### #### ### ### #### ### ### ### ### (API) - -Useful links: - -- Documentation: https://outline.itsnik.de/s/dockstat -- GitHub (Frontend): https://github.com/its4nik/dockstat -- GitHub (Backend): https://github.com/its4nik/dockstatapi -- API Documentation: http://localhost:7000/api-docs - -Summary: - -DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. - -EOF - -npm run start diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 00000000..30602eb0 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,6 @@ +{ + "ignore": ["src/logs", "**/fixtures/**", ".gitignore", "**/*.json"], + "execMap": { + "ts": "tsx" + } +} diff --git a/package-lock.json b/package-lock.json index f4dcffbf..641c0d3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,26 +9,44 @@ "version": "2", "license": "BSD 3-Clause License", "dependencies": { + "@types/dockerode": "^3.3.31", + "@types/supports-color": "^8.1.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.7", "bcrypt": "^5.1.1", - "child_process": "^1.0.2", + "chokidar": "^4.0.1", "cors": "^2.8.5", "dockerode": "^4.0.2", "express": "^4.21.1", "express-rate-limit": "^7.4.1", - "js-yaml": "^4.1.0", + "https": "^1.0.0", + "ipaddr.js": "^2.2.0", "node-fetch": "^3.3.2", "nodemailer": "^6.9.16", - "python-shell": "^5.0.0", "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "twilio": "^5.3.5", - "winston": "^3.15.0", - "yamljs": "^0.3.0" + "winston": "^3.15.0" }, "devDependencies": { + "@playwright/test": "^1.49.0", + "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/express-handlebars": "^5.3.1", + "@types/node": "^22.9.0", + "@types/node-fetch": "^2.6.12", + "@types/nodemailer": "^6.4.17", "dependency-cruiser": "^16.5.0", - "nodemon": "^3.1.7" + "nodemon": "^3.1.7", + "ora": "^8.1.1", + "pkg": "^5.8.1", + "ts-node": "^10.9.2", + "tsx": "^4.19.2", + "uglify-js": "^3.19.3" + }, + "engines": { + "npm": ">=10.8.2" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -75,6 +93,69 @@ "openapi-types": ">=7" } }, + "node_modules/@babel/generator": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", + "integrity": "sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.2", + "@jridgewell/gen-mapping": "^0.3.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", + "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", + "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.18.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@balena/dockerignore": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", @@ -90,6 +171,19 @@ "node": ">=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@dabh/diagnostics": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", @@ -101,199 +195,924 @@ "kuler": "^2.0.0" } }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "optional": true - }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "license": "ISC", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, - "dependencies": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "deprecated": "This functionality has been moved to @npmcli/fs", + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, + "os": [ + "darwin" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 6" + "node": ">=18" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 0.6" + "node": ">=18" } }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.4.0" + "node": ">=18" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/acorn-jsx-walk": { - "version": "2.0.0", + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "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/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@playwright/test": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz", + "integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.49.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "3.3.32", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.32.tgz", + "integrity": "sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg==", + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-handlebars": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@types/express-handlebars/-/express-handlebars-5.3.1.tgz", + "integrity": "sha512-DSzaERLO4gHb8AqnrL58jzSDyT0yDdl6HqDc+bGz1Hf0nrG1FK30nHGzv8NBEGR8QV9eUGB/YaE0Qj3NjF7siw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", + "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/ssh2": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz", + "integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.67", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", + "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/@types/supports-color": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", + "integrity": "sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==", + "license": "MIT" + }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz", + "integrity": "sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-jsx-walk": { + "version": "2.0.0", "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", "dev": true, @@ -406,6 +1225,26 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansi-styles/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": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ansi-styles/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/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -420,6 +1259,19 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -427,28 +1279,31 @@ "license": "ISC" }, "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", "deprecated": "This package is no longer supported.", "license": "ISC", - "optional": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=10" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/array-flatten": { "version": "1.1.1", @@ -456,6 +1311,16 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -475,17 +1340,17 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, "license": "MIT" }, - "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" } }, "node_modules/balanced-match": { @@ -537,12 +1402,6 @@ "tweetnacl": "^0.14.3" } }, - "node_modules/bcrypt/node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "license": "MIT" - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -580,6 +1439,7 @@ "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -603,6 +1463,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -610,7 +1471,8 @@ "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -659,12 +1521,6 @@ "ieee754": "^1.1.13" } }, - "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/buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -678,6 +1534,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -712,26 +1569,16 @@ "node": ">= 10" } }, - "node_modules/cacache/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=10" - } - }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" }, "engines": { "node": ">= 0.4" @@ -740,6 +1587,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/call-me-maybe": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", @@ -747,81 +1607,41 @@ "license": "MIT" }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/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/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/child_process": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", - "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==", - "license": "ISC" - }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" } }, "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } }, "node_modules/clean-stack": { "version": "2.2.0", @@ -833,6 +1653,47 @@ "node": ">=6" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -844,22 +1705,18 @@ } }, "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, + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "color-name": "1.1.3" } }, "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==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, "node_modules/color-string": { @@ -881,21 +1738,6 @@ "color-support": "bin.js" } }, - "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, "node_modules/colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", @@ -910,6 +1752,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -919,12 +1762,13 @@ } }, "node_modules/commander": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", - "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=18" } }, "node_modules/concat-map": { @@ -955,6 +1799,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -974,6 +1819,13 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -1001,6 +1853,13 @@ "node": ">=10.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -1010,19 +1869,13 @@ "node": ">= 12" } }, - "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT" - }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1061,6 +1914,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -1077,6 +1931,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -1092,18 +1947,19 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/dependency-cruiser": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.5.0.tgz", - "integrity": "sha512-6IELC3qRumlwhnbPLmcOK6WWdiGPFBw9a+D8DUsnTFpZ81tEtkAud4OPmU3OJFcuWS5VpgvKlctFkby5XDsGzQ==", + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.7.0.tgz", + "integrity": "sha512-522LLjHINl9r0RIZ8/6s6TqIHTuEJG3XDU2WPSm9dG0rvLUYVyQwE9ID31tDFs4OOyEhdOPaqAaAG1jRv/Zwbg==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.13.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "acorn-jsx-walk": "^2.0.0", "acorn-loose": "^8.4.0", @@ -1123,8 +1979,8 @@ "safe-regex": "^2.1.1", "semver": "^7.6.3", "teamcity-service-messages": "^0.1.14", - "tsconfig-paths-webpack-plugin": "^4.1.0", - "watskeburt": "^4.1.0" + "tsconfig-paths-webpack-plugin": "^4.2.0", + "watskeburt": "^4.1.1" }, "bin": { "depcruise": "bin/dependency-cruise.mjs", @@ -1138,33 +1994,11 @@ "node": "^18.17||>=20" } }, - "node_modules/dependency-cruiser/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/dependency-cruiser/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -1179,6 +2013,29 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/docker-modem": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", @@ -1220,19 +2077,25 @@ "node": ">=6.0.0" } }, - "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", + "node_modules/dunder-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", + "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", + "license": "MIT", "dependencies": { - "safe-buffer": "^5.0.1" + "call-bind-apply-helpers": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -1250,6 +2113,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1318,12 +2182,10 @@ "optional": true }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -1332,14 +2194,66 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" }, "node_modules/esutils": { "version": "2.0.3", @@ -1354,6 +2268,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1368,9 +2283,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -1392,7 +2307,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -1407,6 +2322,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-rate-limit": { @@ -1446,6 +2365,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", @@ -1453,6 +2389,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -1505,6 +2451,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -1522,6 +2469,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -1529,7 +2477,8 @@ "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/fn.name": { "version": "1.1.0", @@ -1537,30 +2486,11 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/form-data": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -1596,16 +2526,77 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/from2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/from2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/from2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -1625,9 +2616,9 @@ "license": "ISC" }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1643,41 +2634,69 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", "deprecated": "This package is no longer supported.", "license": "ISC", - "optional": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" + "wide-align": "^1.1.2" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=10" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz", + "integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -1686,6 +2705,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1742,22 +2774,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/global-directory/node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1770,6 +2824,16 @@ "devOptional": true, "license": "ISC" }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -1784,6 +2848,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -1791,21 +2856,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1823,6 +2878,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -1841,6 +2897,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -1867,6 +2924,12 @@ "node": ">= 6" } }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", + "license": "ISC" + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -1894,6 +2957,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -1983,10 +3047,14 @@ "license": "ISC" }, "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, "node_modules/interpret": { "version": "3.1.1", @@ -1998,6 +3066,23 @@ "node": ">=10.13.0" } }, + "node_modules/into-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", + "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -2012,20 +3097,13 @@ "node": ">= 12" } }, - "node_modules/ip-address/node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause", - "optional": true - }, "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">= 10" } }, "node_modules/is-arrayish": { @@ -2112,6 +3190,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -2154,6 +3245,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2173,12 +3284,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/js-yaml/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, "node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", @@ -2186,6 +3291,19 @@ "license": "MIT", "optional": true }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -2206,47 +3324,17 @@ "node": ">=6" } }, - "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, "license": "MIT", "dependencies": { - "jws": "^3.2.2", - "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" + "universalify": "^2.0.0" }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "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": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "license": "MIT", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, "node_modules/kleur": { @@ -2271,64 +3359,52 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "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.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "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.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "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/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/logform": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", - "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", "license": "MIT", "dependencies": { "@colors/colors": "1.6.0", @@ -2379,6 +3455,13 @@ "semver": "bin/semver.js" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/make-fetch-happen": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", @@ -2411,6 +3494,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2435,10 +3519,21 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -2448,10 +3543,38 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -2640,15 +3763,40 @@ "license": "MIT" }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multistream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + } + }, "node_modules/nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", "license": "MIT", "optional": true }, @@ -2680,9 +3828,9 @@ } }, "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", "license": "MIT" }, "node_modules/node-domexception": { @@ -2747,6 +3895,59 @@ "node": ">= 10.12.0" } }, + "node_modules/node-gyp/node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/nodemailer": { "version": "6.9.16", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", @@ -2778,11 +3979,62 @@ "nodemon": "bin/nodemon.js" }, "engines": { - "node": ">=10" + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/nodemon/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" } }, "node_modules/nopt": { @@ -2811,20 +4063,16 @@ } }, "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", "deprecated": "This package is no longer supported.", "license": "ISC", - "optional": true, "dependencies": { - "are-we-there-yet": "^3.0.0", + "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", + "gauge": "^3.0.0", "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/object-assign": { @@ -2837,9 +4085,10 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2851,6 +4100,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -2873,79 +4123,442 @@ "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", "license": "MIT", "dependencies": { - "fn.name": "1.x.x" + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "node_modules/ora": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.1.1.tgz", + "integrity": "sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.8.1.tgz", + "integrity": "sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "7.18.2", + "@babel/parser": "7.18.4", + "@babel/types": "7.19.0", + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "globby": "^11.1.0", + "into-stream": "^6.0.0", + "is-core-module": "2.9.0", + "minimist": "^1.2.6", + "multistream": "^4.1.0", + "pkg-fetch": "3.4.2", + "prebuild-install": "7.1.1", + "resolve": "^1.22.0", + "stream-meter": "^1.0.4" + }, + "bin": { + "pkg": "lib-es5/bin.js" + }, + "peerDependencies": { + "node-notifier": ">=9.0.1" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/pkg-fetch": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.4.2.tgz", + "integrity": "sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.6", + "progress": "^2.0.3", + "semver": "^7.3.5", + "tar-fs": "^2.1.1", + "yargs": "^16.2.0" + }, + "bin": { + "pkg-fetch": "lib-es5/bin.js" + } + }, + "node_modules/pkg-fetch/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/pkg-fetch/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-fetch/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/pkg-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/pkg-fetch/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/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "node_modules/pkg-fetch/node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "node_modules/pkg/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "aggregate-error": "^3.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "node_modules/pkg/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": ">= 0.8" + "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "node_modules/pkg/node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "node_modules/pkg/node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", "dev": true, - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "node_modules/pkg/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": "ISC" + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/playwright": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", + "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.49.0" + }, + "bin": { + "playwright": "cli.js" + }, "engines": { - "node": ">=8.6" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", + "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" } }, "node_modules/prebuild-install": { @@ -2974,6 +4587,23 @@ "node": ">=10" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -3022,11 +4652,14 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } }, "node_modules/pstree.remy": { "version": "1.1.8", @@ -3036,28 +4669,20 @@ "license": "MIT" }, "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, - "node_modules/python-shell": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/python-shell/-/python-shell-5.0.0.tgz", - "integrity": "sha512-RUOOOjHLhgR1MIQrCtnEqz/HJ1RMZBIN+REnpSUrfft2bXqXy69fwJASVziWExfFXsR1bCY0TznnHooNsCo0/w==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" }, @@ -3068,10 +4693,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -3080,6 +4727,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -3105,6 +4753,12 @@ "rc": "cli.js" } }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -3120,16 +4774,16 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/rechoir": { @@ -3155,6 +4809,16 @@ "regexp-tree": "bin/regexp-tree" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3183,6 +4847,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/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/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -3193,6 +4897,17 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -3200,13 +4915,37 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "license": "ISC", "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" } }, "node_modules/safe-buffer": { @@ -3240,9 +4979,9 @@ } }, "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", "license": "MIT", "engines": { "node": ">=10" @@ -3254,12 +4993,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/scmp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", - "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==", - "license": "BSD-3-Clause" - }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -3276,6 +5009,7 @@ "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -3299,6 +5033,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -3306,25 +5041,23 @@ "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/send/node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", @@ -3345,6 +5078,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -3360,12 +5094,14 @@ "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", @@ -3459,6 +5195,16 @@ "dev": true, "license": "MIT" }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -3507,10 +5253,11 @@ "license": "ISC" }, "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true }, "node_modules/sqlite3": { "version": "5.1.7", @@ -3536,10 +5283,16 @@ } } }, + "node_modules/sqlite3/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/ssh2": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz", - "integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", + "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", "hasInstallScript": true, "dependencies": { "asn1": "^0.2.6", @@ -3549,8 +5302,8 @@ "node": ">=10.16.0" }, "optionalDependencies": { - "cpu-features": "~0.0.9", - "nan": "^2.18.0" + "cpu-features": "~0.0.10", + "nan": "^2.20.0" } }, "node_modules/ssri": { @@ -3579,10 +5332,67 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stream-meter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", + "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.1.4" + } + }, + "node_modules/stream-meter/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stream-meter/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-meter/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3683,6 +5493,15 @@ "node": ">=12.0.0" } }, + "node_modules/swagger-jsdoc/node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/swagger-jsdoc/node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -3717,10 +5536,13 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.17.14", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", - "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", - "license": "Apache-2.0" + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz", + "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } }, "node_modules/swagger-ui-express": { "version": "5.0.1", @@ -3776,6 +5598,12 @@ "tar-stream": "^2.0.0" } }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -3792,15 +5620,6 @@ "node": ">=6" } }, - "node_modules/tar/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/tar/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -3823,6 +5642,16 @@ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3840,6 +5669,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", "engines": { "node": ">=0.6" } @@ -3869,6 +5699,50 @@ "node": ">= 14.0.0" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -3885,20 +5759,96 @@ } }, "node_modules/tsconfig-paths-webpack-plugin": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz", - "integrity": "sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", "tsconfig-paths": "^4.1.2" }, "engines": { "node": ">=10.13.0" } }, + "node_modules/tsconfig-paths-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/tsconfig-paths-webpack-plugin/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/tsconfig-paths-webpack-plugin/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/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -3917,28 +5867,11 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "license": "Unlicense" }, - "node_modules/twilio": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.3.5.tgz", - "integrity": "sha512-f/sA1Yd6TyIzfcq0u4QDGU+93afwswsJB+rf3T08tvBAMobBDVR3DfGREwJr5jp8xUic0qWa7GbJidk16NA4bg==", - "license": "MIT", - "dependencies": { - "axios": "^1.7.4", - "dayjs": "^1.11.9", - "https-proxy-agent": "^5.0.0", - "jsonwebtoken": "^9.0.2", - "qs": "^6.9.4", - "scmp": "^2.1.0", - "xmlbuilder": "^13.0.2" - }, - "engines": { - "node": ">=14.0" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -3947,6 +5880,34 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -3954,6 +5915,12 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -3974,10 +5941,21 @@ "imurmurhash": "^0.1.4" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -3997,6 +5975,13 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/validator": { "version": "13.12.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", @@ -4016,9 +6001,9 @@ } }, "node_modules/watskeburt": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-4.1.0.tgz", - "integrity": "sha512-KkY5H51ajqy9HYYI+u9SIURcWnqeVVhdH0I+ab6aXPGHfZYxgRCwnR6Lm3+TYB6jJVt5jFqw4GAKmwf1zHmGQw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-4.2.2.tgz", + "integrity": "sha512-AOCg1UYxWpiHW1tUwqpJau8vzarZYTtzl2uu99UptBmbzx6kOzCGMfRLF6KIRX4PYekmryn89MzxlRNkL66YyA==", "dev": true, "license": "MIT", "bin": { @@ -4079,34 +6064,34 @@ } }, "node_modules/winston": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.15.0.tgz", - "integrity": "sha512-RhruH2Cj0bV0WgNL+lOfoUBI4DVfdUNjVnJGVovWZmrcKtrFTTRzgXYK2O9cymSGjrERCtaAeHwMNnUWXlwZow==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", - "logform": "^2.6.0", + "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", - "winston-transport": "^4.7.0" + "winston-transport": "^4.9.0" }, "engines": { "node": ">= 12.0.0" } }, "node_modules/winston-transport": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.1.tgz", - "integrity": "sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", "license": "MIT", "dependencies": { - "logform": "^2.6.1", + "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" }, @@ -4114,19 +6099,38 @@ "node": ">= 12.0.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "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/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/xmlbuilder": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", - "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", - "license": "MIT", + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", "engines": { - "node": ">=6.0" + "node": ">=10" } }, "node_modules/yallist": { @@ -4144,18 +6148,43 @@ "node": ">= 6" } }, - "node_modules/yamljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", - "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "glob": "^7.0.5" + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, - "bin": { - "json2yaml": "bin/json2yaml", - "yaml2json": "bin/yaml2json" + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/z-schema": { diff --git a/package.json b/package.json index b1c3b7c4..78ac9460 100644 --- a/package.json +++ b/package.json @@ -2,43 +2,68 @@ "name": "dockstatapi", "version": "2", "description": "API for docker hosts using dockerode", - "main": "server.js", + "main": "src/server.ts", "scripts": { - "start": "node server.js", - "dev": "nodemon server.js", - "offline": "OFFLINE=true nodemon server.js", - "dep": "bash ./utils/createDependencyGraph.sh" + "start": "tsx src/server.ts", + "start:build": "npx tsc && node dist/server.js", + "dev": "nodemon", + "dev:trace": "nodemon --trace-uncaught --trace-warnings", + "dep": "bash ./src/utils/createDependencyGraph.sh", + "dep:remove": "bash ./src/utils/removeUnusedDeps.sh && bash ./src/utils/createDependencyGraph.sh", + "build": "npx tsc", + "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", + "mini": "bash ./src/misc/minifyDist.sh" }, "keywords": [], "author": "Its4Nik", "license": "BSD 3-Clause License", "dependencies": { + "@types/dockerode": "^3.3.31", + "@types/supports-color": "^8.1.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.7", "bcrypt": "^5.1.1", - "child_process": "^1.0.2", + "chokidar": "^4.0.1", "cors": "^2.8.5", "dockerode": "^4.0.2", "express": "^4.21.1", "express-rate-limit": "^7.4.1", - "js-yaml": "^4.1.0", + "https": "^1.0.0", + "ipaddr.js": "^2.2.0", "node-fetch": "^3.3.2", "nodemailer": "^6.9.16", - "python-shell": "^5.0.0", "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "twilio": "^5.3.5", - "winston": "^3.15.0", - "yamljs": "^0.3.0" + "winston": "^3.15.0" }, "devDependencies": { + "@playwright/test": "^1.49.0", + "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/express-handlebars": "^5.3.1", + "@types/node": "^22.9.0", + "@types/node-fetch": "^2.6.12", + "@types/nodemailer": "^6.4.17", "dependency-cruiser": "^16.5.0", - "nodemon": "^3.1.7" + "nodemon": "^3.1.7", + "ora": "^8.1.1", + "pkg": "^5.8.1", + "ts-node": "^10.9.2", + "tsx": "^4.19.2", + "uglify-js": "^3.19.3" }, "nodemonConfig": { "ignore": [ - "**/logs/**", - "**/data/**" + "**/data/**", + "**/*.json", + ".gitignore" ], "delay": 2500 - } + }, + "engines": { + "npm": ">=10.8.2" + }, + "repository": "git@github.com:Its4Nik/dockstatapi.git" } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..2c33a93e --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,37 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + timeout: 300000, + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + trace: 'on-first-retry', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], + + webServer: { + command: 'npm run start', + url: 'http://127.0.0.1:9876', + reuseExistingServer: true + }, +}); diff --git a/routes/auth/routes.js b/routes/auth/routes.js deleted file mode 100644 index bbc20d4b..00000000 --- a/routes/auth/routes.js +++ /dev/null @@ -1,146 +0,0 @@ -const express = require("express"); -const bcrypt = require("bcrypt"); -const fs = require("fs"); -const path = require("path"); -const logger = require("../../utils/logger"); -const router = express.Router(); -const passwordFile = path.join(__dirname, "../../middleware/password.json"); -const passwordBool = path.join(__dirname, "../../middleware/usePassword.txt"); -const saltRounds = 10; - -function setTrue() { - fs.writeFile(passwordBool, "true", "utf8", (err) => { - if (err) { - logger.error("Error writing to the file:", err); - return; - } - logger.info(`Status "true" has been written to the file.`); - }); -} - -function setFalse() { - fs.writeFile(passwordBool, "false", "utf8", (err) => { - if (err) { - logger.error("Error writing to the file:", err); - return; - } - logger.info(`Status "false" has been written to the file.`); - }); -} - -/** - * @swagger - * /auth/enable: - * post: - * summary: Enable authentication by setting a password - * tags: [Authentication] - * parameters: - * - name: password - * in: query - * required: true - * responses: - * 200: - * description: Authentication enabled. - * 400: - * description: Password is required. - * 500: - * description: Error saving password. - */ -router.post("/enable", (req, res) => { - fs.readFile(passwordBool, "utf8", (err, data) => { - const password = req.query.password; - if (err) { - logger.error("Error reading the file:", err); - return; - } - - const isAuthEnabled = data.trim() === "true"; - if (isAuthEnabled) { - logger.error( - "Passowrd Authentication is already enabled, please dactivate it first", - ); - return res.status(401).json({ - message: - "Passowrd Authentication is already enabled, please dactivate it first", - }); - } - - if (!password) { - return res.status(400).json({ message: "Password is required" }); - } - - bcrypt.genSalt(saltRounds, (err, salt) => { - if (err) { - logger.error("Error generating salt"); - return res.status(500).json({ message: "Error generating salt" }); - } - - bcrypt.hash(password, salt, (err, hash) => { - if (err) { - logger.error("Error hashing password"); - return res.status(500).json({ message: "Error hashing password" }); - } - - const passwordData = { hash, salt }; - fs.writeFile(passwordFile, JSON.stringify(passwordData), (err) => { - if (err) { - return res.status(500).json({ message: "Error saving password" }); - } - setTrue(); - res.json({ message: "Authentication enabled" }); - }); - }); - }); - }); -}); - -/** - * @swagger - * /auth/disable: - * post: - * summary: Disable authentication by providing the existing password - * tags: [Authentication] - * parameters: - * - name: password - * in: query - * required: true - * responses: - * 200: - * description: Authentication disabled. - * 400: - * description: Password is required. - * 401: - * description: Invalid password. - * 500: - * description: Error disabling authentication. - */ -router.post("/disable", (req, res) => { - const password = req.query.password; - if (!password) { - logger.error("Password is required!"); - return res.status(400).json({ message: "Password is required" }); - } - - fs.readFile(passwordFile, "utf8", (err, data) => { - if (err) { - logger.error("Error reading password"); - return res.status(500).json({ message: "Error reading password" }); - } - - const storedData = JSON.parse(data); - bcrypt.compare(password, storedData.hash, (err, result) => { - if (err) { - logger.error("Error validating password"); - return res.status(500).json({ message: "Error validating password" }); - } - if (!result) { - logger.error("Invalid password"); - return res.status(401).json({ message: "Invalid password" }); - } - setFalse(); - res.json({ message: "Authentication disabled" }); - }); - }); -}); - -module.exports = router; diff --git a/routes/data/routes.js b/routes/data/routes.js deleted file mode 100644 index adce8d79..00000000 --- a/routes/data/routes.js +++ /dev/null @@ -1,111 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const db = require("../../config/db"); -const logger = require("../../utils/logger"); - -function formatRows(rows) { - return rows.reduce((acc, row, index) => { - acc[index] = JSON.parse(row.info); - return acc; - }, {}); -} - -/** - * @swagger - * /data/latest: - * get: - * summary: Retrieve the latest entry from the database - * tags: [Database queries] - * responses: - * 200: - * description: A JSON object containing the latest entry's 'info' data. - * content: - * application/json: - * schema: - * type: object - * example: - * name: "Container A" - * id: "abcd1234" - * cpu_usage: 30 - * mem_usage: 2048 - */ -router.get("/latest", (req, res) => { - db.get( - "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", - (err, row) => { - if (err) { - logger.error("Error fetching latest data:", err.message); - return res.status(500).json({ error: "Internal server error" }); - } - res.json(JSON.parse(row.info)); - }, - ); -}); - -/** - * @swagger - * /data/time/24h: - * get: - * summary: Retrieve entries from the last 24 hours from the database - * tags: [Database queries] - * responses: - * 200: - * description: A numbered array of 'info' JSON objects from the last 24 hours. - * content: - * application/json: - * schema: - * type: object - * example: - * 0: - * name: "Container A" - * id: "abcd1234" - * cpu_usage: 30 - * mem_usage: 2048 - * 1: - * name: "Container B" - * id: "efgh5678" - * cpu_usage: 45 - * mem_usage: 3072 - */ -router.get("/time/24h", (req, res) => { - const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); - db.all( - "SELECT info FROM data WHERE timestamp >= ?", - [oneDayAgo], - (err, rows) => { - if (err) { - logger.error("Error fetching data from last 24 hours:", err.message); - return res.status(500).json({ error: "Internal server error" }); - } - res.json(formatRows(rows)); - }, - ); -}); - -/** - * @swagger - * /data/clear: - * delete: - * summary: Clear all entries from the database - * tags: [Database queries] - * responses: - * 200: - * description: A message indicating whether the database was cleared successfully. - * content: - * application/json: - * schema: - * type: object - * example: - * message: "Database cleared successfully." - */ -router.delete("/clear", (req, res) => { - db.run("DELETE FROM data", (err) => { - if (err) { - logger.error("Error clearing the database:", err.message); - return res.status(500).json({ error: "Internal server error" }); - } - res.json({ message: "Database cleared successfully" }); - }); -}); - -module.exports = router; diff --git a/routes/setter/routes.js b/routes/setter/routes.js deleted file mode 100644 index 24ae2ad9..00000000 --- a/routes/setter/routes.js +++ /dev/null @@ -1,145 +0,0 @@ -const { - setFetchInterval, - parseInterval, -} = require("../../controllers/scheduler"); -const express = require("express"); -const router = express.Router(); -const path = require("path"); -const fs = require("fs"); -const logger = require("../../utils/logger"); - -/** - * @swagger - * /conf/addHost: - * put: - * summary: Add a new host to the Docker configuration - * tags: [Configuration] - * parameters: - * - name: name - * in: query - * required: true - * description: The name of the new host. - * - name: url - * in: query - * required: true - * description: The URL of the new host. - * - name: port - * in: query - * required: true - * description: The port of the new host. - * responses: - * 200: - * description: Host added successfully. - * 400: - * description: Bad request, invalid input. - * 500: - * description: An error occurred while adding the host. - */ -router.put("/addHost", async (req, res) => { - const name = req.query.name; - const url = req.query.url; - const port = req.query.port; - const configPath = path.join(__dirname, "../../config/dockerConfig.json"); - - if (!name || !url || !port) { - return res.status(400).json({ error: "Name, Port and URL are required." }); - } - - try { - const rawData = fs.readFileSync(configPath); - const config = JSON.parse(rawData); - - // Check for existing host - if (config.hosts.some((host) => host.name === name)) { - return res.status(400).json({ error: "Host already exists." }); - } - - config.hosts.push({ name, url, port }); - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); - logger.info(`Added new host: ${name}`); - res.status(200).json({ message: "Host added successfully." }); - } catch (error) { - logger.error("Error adding host: " + error.message); - res.status(500).json({ error: "Failed to add host." }); - } -}); - -/** - * @swagger - * /conf/scheduler: - * put: - * summary: Set fetch interval for data fetching - * tags: [Configuration] - * parameters: - * - name: interval - * in: query - * required: true - * description: The new interval for fetching data, e.g., "6h 20m", "300s". - * responses: - * 200: - * description: Fetch interval set successfully. - * 400: - * description: Invalid interval format or out of range. - */ -router.put("/scheduler", (req, res) => { - const interval = req.query.interval; - const newInterval = parseInterval(interval); - - if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { - return res - .status(400) - .json({ error: "Interval must be between 5 minutes and 6 hours." }); - } - - setFetchInterval(newInterval); - res.json({ message: `Fetch interval set to ${interval}.` }); -}); - -/** - * @swagger - * /conf/removeHost: - * delete: - * summary: Remove a host from the Docker configuration - * tags: [Configuration] - * parameters: - * - name: hostName - * in: query - * required: true - * description: The name of the host to remove. - * responses: - * 200: - * description: Host removed successfully. - * 404: - * description: Host not found. - * 500: - * description: An error occurred while removing the host. - */ -router.delete("/removeHost", async (req, res) => { - const hostName = req.query.hostName; - const configPath = path.join(__dirname, "../../config/dockerConfig.json"); - - if (!hostName) { - return res.status(400).json({ error: "Host name is required." }); - } - - try { - const rawData = fs.readFileSync(configPath); - const config = JSON.parse(rawData); - - // Check for existing host - const hostIndex = config.hosts.findIndex((host) => host.name === hostName); - if (hostIndex === -1) { - return res.status(404).json({ error: "Host not found." }); - } - - config.hosts.splice(hostIndex, 1); - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); - logger.info(`Removed host: ${hostName}`); - res.status(200).json({ message: "Host removed successfully." }); - } catch (error) { - logger.error("Error removing host: " + error.message); - res.status(500).json({ error: "Failed to remove host." }); - } -}); - -module.exports = router; diff --git a/server.js b/server.js deleted file mode 100644 index d383cde9..00000000 --- a/server.js +++ /dev/null @@ -1,49 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const app = express(); - -// Utility: -const swaggerDocs = require("./swagger/swaggerDocs"); -const logger = require("./utils/logger"); - -// Routes: -const api = require("./routes/getter/routes"); -const conf = require("./routes/setter/routes"); -const auth = require("./routes/auth/routes"); -const data = require("./routes/data/routes"); -const frontend = require("./routes/frontendController/routes"); -const notificationService = require("./routes/notifications/routes"); - -// Middleware: -const authMiddleware = require("./middleware/authMiddleware"); -const { limiter } = require("./middleware/rateLimiter"); - -// Controllers -const { scheduleFetch } = require("./controllers/scheduler"); - -const PORT = "7070"; - -app.use(express.json()); - -app.use("/api-docs", (req, res, next) => next()); - -swaggerDocs(app); -scheduleFetch(); - -// Routes -app.use("/api", limiter, authMiddleware, api); -app.use("/conf", limiter, authMiddleware, conf); -app.use("/auth", limiter, authMiddleware, auth); -app.use("/data", limiter, authMiddleware, data); -app.use("/frontend", limiter, authMiddleware, frontend); -app.use("/notification-service", limiter, authMiddleware, notificationService); - -// Default route -router.get("/", (req, res) => { - res.redirect("/api-docs"); -}); - -app.listen(PORT, () => { - logger.info(`Server is running on http://localhost:${PORT}`); - logger.info(`Swagger docs available at http://localhost:${PORT}/api-docs`); -}); diff --git a/.dependency-cruiser.js b/src/.dependency-cruiser.cjs similarity index 66% rename from .dependency-cruiser.js rename to src/.dependency-cruiser.cjs index 07df12bf..d734a682 100644 --- a/.dependency-cruiser.js +++ b/src/.dependency-cruiser.cjs @@ -2,87 +2,83 @@ module.exports = { forbidden: [ { - name: 'no-circular', - severity: 'warn', + name: "no-circular", + severity: "warn", comment: - 'This dependency is part of a circular relationship. You might want to revise ' + - 'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ', + "This dependency is part of a circular relationship. You might want to revise " + + "your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ", from: {}, to: { - circular: true - } + circular: true, + }, }, { - name: 'no-orphans', + name: "no-orphans", comment: "This is an orphan module - it's likely not used (anymore?). Either use it or " + "remove it. If it's logical this module is an orphan (i.e. it's a config file), " + "add an exception for it in your dependency-cruiser configuration. By default " + "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", - severity: 'warn', + severity: "warn", from: { orphan: true, pathNot: [ - '(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$', // dot files - '[.]d[.]ts$', // TypeScript declaration files - '(^|/)tsconfig[.]json$', // TypeScript config - '(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$' // other configs - ] + "(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$", // dot files + "[.]d[.]ts$", // TypeScript declaration files + "(^|/)tsconfig[.]json$", // TypeScript config + "(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$", // other configs + ], }, to: {}, }, { - name: 'no-deprecated-core', + name: "no-deprecated-core", comment: - 'A module depends on a node core module that has been deprecated. Find an alternative - these are ' + + "A module depends on a node core module that has been deprecated. Find an alternative - these are " + "bound to exist - node doesn't deprecate lightly.", - severity: 'warn', + severity: "warn", from: {}, to: { - dependencyTypes: [ - 'core' - ], + dependencyTypes: ["core"], path: [ - '^v8/tools/codemap$', - '^v8/tools/consarray$', - '^v8/tools/csvparser$', - '^v8/tools/logreader$', - '^v8/tools/profile_view$', - '^v8/tools/profile$', - '^v8/tools/SourceMap$', - '^v8/tools/splaytree$', - '^v8/tools/tickprocessor-driver$', - '^v8/tools/tickprocessor$', - '^node-inspect/lib/_inspect$', - '^node-inspect/lib/internal/inspect_client$', - '^node-inspect/lib/internal/inspect_repl$', - '^async_hooks$', - '^punycode$', - '^domain$', - '^constants$', - '^sys$', - '^_linklist$', - '^_stream_wrap$' + "^v8/tools/codemap$", + "^v8/tools/consarray$", + "^v8/tools/csvparser$", + "^v8/tools/logreader$", + "^v8/tools/profile_view$", + "^v8/tools/profile$", + "^v8/tools/SourceMap$", + "^v8/tools/splaytree$", + "^v8/tools/tickprocessor-driver$", + "^v8/tools/tickprocessor$", + "^node-inspect/lib/_inspect$", + "^node-inspect/lib/internal/inspect_client$", + "^node-inspect/lib/internal/inspect_repl$", + "^async_hooks$", + "^punycode$", + "^domain$", + "^constants$", + "^sys$", + "^_linklist$", + "^_stream_wrap$", ], - } + }, }, { - name: 'not-to-deprecated', + name: "not-to-deprecated", comment: - 'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' + - 'version of that module, or find an alternative. Deprecated modules are a security risk.', - severity: 'warn', + "This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later " + + "version of that module, or find an alternative. Deprecated modules are a security risk.", + severity: "warn", from: {}, to: { - dependencyTypes: [ - 'deprecated' - ] - } + dependencyTypes: ["deprecated"], + }, }, { - name: 'no-non-package-json', - severity: 'error', + name: "no-non-package-json", + severity: "error", comment: "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + "That's problematic as the package either (1) won't be available on live (2 - worse) will be " + @@ -90,84 +86,75 @@ module.exports = { "in your package.json.", from: {}, to: { - dependencyTypes: [ - 'npm-no-pkg', - 'npm-unknown' - ] - } + dependencyTypes: ["npm-no-pkg", "npm-unknown"], + }, }, { - name: 'not-to-unresolvable', + name: "not-to-unresolvable", comment: "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + - 'module: add it to your package.json. In all other cases you likely already know what to do.', - severity: 'error', + "module: add it to your package.json. In all other cases you likely already know what to do.", + severity: "error", from: {}, to: { - couldNotResolve: true - } + couldNotResolve: true, + }, }, { - name: 'no-duplicate-dep-types', + name: "no-duplicate-dep-types", comment: "Likely this module depends on an external ('npm') package that occurs more than once " + "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + "maintenance problems later on.", - severity: 'warn', + severity: "warn", from: {}, to: { moreThanOneDependencyType: true, - // as it's pretty common to have a type import be a type only import + // as it's pretty common to have a type import be a type only import // _and_ (e.g.) a devDependency - don't consider type-only dependency // types for this rule - dependencyTypesNot: ["type-only"] - } + dependencyTypesNot: ["type-only"], + }, }, /* rules you might want to tweak for your specific situation: */ - + { - name: 'not-to-spec', + name: "not-to-spec", comment: - 'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' + + "This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. " + "If there's something in a spec that's of use to other modules, it doesn't have that single " + - 'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.', - severity: 'error', + "responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.", + severity: "error", from: {}, to: { - path: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' - } + path: "[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$", + }, }, { - name: 'not-to-dev-dep', - severity: 'error', + name: "not-to-dev-dep", + severity: "error", comment: "This module depends on an npm package from the 'devDependencies' section of your " + - 'package.json. It looks like something that ships to production, though. To prevent problems ' + + "package.json. It looks like something that ships to production, though. To prevent problems " + "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + - 'section of your package.json. If this module is development only - add it to the ' + - 'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration', + "section of your package.json. If this module is development only - add it to the " + + "from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration", from: { - path: '^(\./)', - pathNot: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' + path: "^(./)", + pathNot: "[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$", }, to: { - dependencyTypes: [ - 'npm-dev', - ], + dependencyTypes: ["npm-dev"], // type only dependencies are not a problem as they don't end up in the // production code or are ignored by the runtime. - dependencyTypesNot: [ - 'type-only' - ], - pathNot: [ - 'node_modules/@types/' - ] - } + dependencyTypesNot: ["type-only"], + pathNot: ["node_modules/@types/"], + }, }, { - name: 'optional-deps-used', - severity: 'info', + name: "optional-deps-used", + severity: "info", comment: "This module depends on an npm package that is declared as an optional dependency " + "in your package.json. As this makes sense in limited situations only, it's flagged here. " + @@ -175,33 +162,28 @@ module.exports = { "dependency-cruiser configuration.", from: {}, to: { - dependencyTypes: [ - 'npm-optional' - ] - } + dependencyTypes: ["npm-optional"], + }, }, { - name: 'peer-deps-used', + name: "peer-deps-used", comment: "This module depends on an npm package that is declared as a peer dependency " + "in your package.json. This makes sense if your package is e.g. a plugin, but in " + "other cases - maybe not so much. If the use of a peer dependency is intentional " + "add an exception to your dependency-cruiser configuration.", - severity: 'warn', + severity: "warn", from: {}, to: { - dependencyTypes: [ - 'npm-peer' - ] - } - } + dependencyTypes: ["npm-peer"], + }, + }, ], options: { - /* Which modules not to follow further when encountered */ doNotFollow: { /* path: an array of regular expressions in strings to match against */ - path: ['node_modules'] + path: ["../node_modules"], }, /* Which modules to exclude */ @@ -220,15 +202,15 @@ module.exports = { module systems it knows of. It's the default because it's the safe option It might come at a performance penalty, though. moduleSystems: ['amd', 'cjs', 'es6', 'tsd'] - + As in practice only commonjs ('cjs') and ecmascript modules ('es6') are widely used, you can limit the moduleSystems to those. */ - + // moduleSystems: ['cjs', 'es6'], /* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/main/' - to open it on your online repo or `vscode://file/${process.cwd()}/` to + to open it on your online repo or `vscode://file/${process.cwd()}/` to open it in visual studio code), */ // prefix: `vscode://file/${process.cwd()}/`, @@ -238,7 +220,7 @@ module.exports = { "specify": for each dependency identify whether it only exists before compilation or also after */ // tsPreCompilationDeps: false, - + /* list of extensions to scan that aren't javascript or compile-to-javascript. Empty by default. Only put extensions in here that you want to take into account that are _not_ parsable. @@ -262,9 +244,9 @@ module.exports = { dependency-cruiser's current working directory). When not provided defaults to './tsconfig.json'. */ - // tsConfig: { - // fileName: 'tsconfig.json' - // }, + //tsConfig: { + //fileName: "../tsconfig.json", + //}, /* Webpack configuration to use to get resolve options from. @@ -273,7 +255,7 @@ module.exports = { to './webpack.conf.js'. The (optional) `env` and `arguments` attributes contain the parameters - to be passed if your webpack config is a function and takes them (see + to be passed if your webpack config is a function and takes them (see webpack documentation for details) */ // webpackConfig: { @@ -295,7 +277,7 @@ module.exports = { a hack. */ // exoticRequireStrings: [], - + /* options to pass on to enhanced-resolve, the package dependency-cruiser uses to resolve module references to disk. The values below should be suitable for most situations @@ -304,7 +286,7 @@ module.exports = { there will override the ones specified here. */ enhancedResolveOptions: { - /* What to consider as an 'exports' field in package.jsons */ + /* What to consider as an 'exports' field in package.jsons */ exportsFields: ["exports"], /* List of conditions to check for in the exports field. Only works when the 'exportsFields' array is non-empty. @@ -314,22 +296,18 @@ module.exports = { The extensions, by default are the same as the ones dependency-cruiser can access (run `npx depcruise --info` to see which ones that are in _your_ environment). If that list is larger than you need you can pass - the extensions you actually use (e.g. [".js", ".jsx"]). This can speed + the extensions you actually use (e.g. ["", ".jsx"]). This can speed up module resolution, which is the most expensive step. */ - // extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"], + extensions: ["", ".jsx", ".ts", ".tsx"], /* What to consider a 'main' field in package.json */ - - // if you migrate to ESM (or are in an ESM environment already) you will want to - // have "module" in the list of mainFields, like so: - // mainFields: ["module", "main", "types", "typings"], - mainFields: ["main", "types", "typings"], + mainFields: ["module", "main", "types", "typings"], /* A list of alias fields in package.jsons See [this specification](https://github.com/defunctzombie/package-browser-field-spec) and the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields) - documentation - + documentation + Defaults to an empty array (= don't use alias fields). */ // aliasFields: ["browser"], @@ -341,21 +319,21 @@ module.exports = { collapses everything in node_modules to one folder deep so you see the external modules, but their innards. */ - collapsePattern: 'node_modules/(?:@[^/]+/[^/]+|[^/]+)', + collapsePattern: "node_modules/(?:@[^/]+/[^/]+|[^/]+)", /* Options to tweak the appearance of your graph.See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions for details and some examples. If you don't specify a theme dependency-cruiser falls back to a built-in one. */ - // theme: { - // graph: { - // /* splines: "ortho" gives straight lines, but is slow on big graphs - // splines: "true" gives bezier curves (fast, not as nice as ortho) - // */ - // splines: "true" - // }, - // } + theme: { + graph: { + /* splines: "ortho" gives straight lines, but is slow on big graphs + splines: "true" gives bezier curves (fast, not as nice as ortho) + */ + ortho: "true", + }, + }, }, archi: { /* pattern of modules that can be consolidated in the high level @@ -363,7 +341,8 @@ module.exports = { dependency graph reporter (`archi`) you probably want to tweak this collapsePattern to your situation. */ - collapsePattern: '^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)', + collapsePattern: + "^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)", /* Options to tweak the appearance of your graph. If you don't specify a theme for 'archi' dependency-cruiser will use the one specified in the @@ -371,10 +350,10 @@ module.exports = { */ // theme: { }, }, - "text": { - "highlightFocused": true + text: { + highlightFocused: true, }, - } - } + }, + }, }; -// generated: dependency-cruiser@16.5.0 on 2024-10-31T20:09:59.974Z +// generated: dependency-cruiser@16.5.0 on 2024-11-08T20:57:37.261Z diff --git a/src/config/db.ts b/src/config/db.ts new file mode 100644 index 00000000..93972131 --- /dev/null +++ b/src/config/db.ts @@ -0,0 +1,30 @@ +import sqlite3 from "sqlite3"; +import logger from "../utils/logger"; + +const dbPath: string = "./src/data/database.db"; + +const db: sqlite3.Database = new sqlite3.Database( + dbPath, + (err: Error | null) => { + if (err) { + logger.error("Error opening database:", err.message); + } else { + db.run( + `CREATE TABLE IF NOT EXISTS data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + info TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + (tableErr: Error | null) => { + if (tableErr) { + logger.error("Error creating table:", tableErr.message); + } else { + logger.info("Database created / opened successfully"); + } + }, + ); + } + }, +); + +export default db; diff --git a/src/config/hostsystem.ts b/src/config/hostsystem.ts new file mode 100644 index 00000000..520d3efd --- /dev/null +++ b/src/config/hostsystem.ts @@ -0,0 +1,61 @@ +import fs from "fs"; +import logger from "../utils/logger"; +import os from "os"; + +const userConf = "./src/data/user.conf"; +const inDocker: boolean = !!process.env.RUNNING_IN_DOCKER; +const version: string = process.env.VERSION || "unknown"; + +function writeUserConf() { + let previousConfig = null; + let shouldRewriteConfig = false; + + const installationDetails = { + installedAt: new Date().toISOString(), + backendVersion: version, + inDocker: inDocker, + installedBy: os.userInfo().username, + platform: os.platform(), + arch: os.arch(), + }; + + if (fs.existsSync(userConf)) { + try { + previousConfig = JSON.parse(fs.readFileSync(userConf, "utf-8")); + if (previousConfig.backendVersion !== version) { + shouldRewriteConfig = true; + logger.debug( + "Version change detected. Rewriting configuration file...", + ); + } else { + logger.debug("No version change detected. Skipping re-initialization."); + } + } catch (error) { + logger.error( + "Error reading the configuration file. Rewriting it...", + error, + ); + shouldRewriteConfig = true; + } + } else { + logger.debug("Configuration file not found. Creating a new one..."); + shouldRewriteConfig = true; + } + + if (shouldRewriteConfig) { + fs.writeFileSync(userConf, JSON.stringify(installationDetails, null, 2)); + logger.debug("Configuration file created/updated:", userConf); + } + + const startDetails = { + startedAt: new Date().toISOString(), + backendVersion: version, + }; + + logger.info("Starting the server..."); + logger.info( + `At: ${startDetails.startedAt} - Version: ${startDetails.backendVersion} - Docker: ${installationDetails.inDocker} - Installed as: ${installationDetails.installedBy} - Platform: ${installationDetails.platform} - Arch: ${installationDetails.arch}`, + ); +} + +export default writeUserConf; diff --git a/src/config/loggerConfig.ts b/src/config/loggerConfig.ts new file mode 100644 index 00000000..7d34f035 --- /dev/null +++ b/src/config/loggerConfig.ts @@ -0,0 +1,45 @@ +import { createLogger, format, transports } from "winston"; + +const gray = "\x1b[90m"; +const reset = "\x1b[0m"; +const white = "\x1b[97m"; +const red = "\x1b[31m"; +const green = "\x1b[32m"; +const yellow = "\x1b[33m"; +const blue = "\x1b[34m"; + +function colorLog(level: string, levelName: string) { + switch (level) { + case "info": + return `${green}${levelName}${reset}`; + case "debug": + return `${blue}${levelName}${reset}`; + case "error": + return `${red}${levelName}${reset}`; + case "warn": + return `${yellow}${levelName}${reset}`; + default: + return `${gray}UNKNOWN${reset}`; + } +} + +const logger = createLogger({ + level: "debug", + format: format.combine( + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + format.printf((info) => { + const level = info.level.toUpperCase().padEnd(5, " "); + const timestamp = `${gray}${info.timestamp}${reset}`; + const levelColorized = colorLog(info.level.toLowerCase(), level); + const message = `${white}${info.message}${reset}`; + + return `${timestamp} ${levelColorized} : ${message}`; + }), + ), + transports: [ + new transports.Console(), + new transports.File({ filename: "logs/app.log" }), + ], +}); + +export default logger; diff --git a/src/config/swaggerConfig.ts b/src/config/swaggerConfig.ts new file mode 100644 index 00000000..630805e9 --- /dev/null +++ b/src/config/swaggerConfig.ts @@ -0,0 +1,53 @@ +const options: { + definition: { + failOnErrors: boolean; + openapi: string; + info: { + title: string; + version: string; + description: string; + }; + components: { + securitySchemes: { + passwordAuth: { + type: string; + in: string; + name: string; + description: string; + }; + }; + }; + security: Array<{ + passwordAuth: any[]; + }>; + }; + apis: string[]; +} = { + definition: { + failOnErrors: true, + openapi: "3.0.0", + info: { + title: "DockStatAPI", + version: "2", + description: "An API used to query muliple docker hosts", + }, + components: { + securitySchemes: { + passwordAuth: { + type: "apiKey", + in: "header", + name: "x-password", + description: "Password required for authentication", + }, + }, + }, + security: [ + { + passwordAuth: [], + }, + ], + }, + apis: ["./src/routes/*/*.ts"], +}; + +export default options; diff --git a/controllers/containerController.js b/src/controllers/containerController.ts similarity index 58% rename from controllers/containerController.js rename to src/controllers/containerController.ts index f62ec5ce..1532681e 100644 --- a/controllers/containerController.js +++ b/src/controllers/containerController.ts @@ -1,27 +1,30 @@ -const fs = require("fs"); -const path = require("path"); -const { getDockerClient } = require("../utils/dockerClient"); -const logger = require("../utils/logger"); +import getDockerClient from "../utils/dockerClient"; +import logger from "../utils/logger"; +import { Request, Response } from "express"; -const getContainers = async (req, res) => { - const host = req.query.host || "local"; +const getContainers = async (req: Request, res: Response): Promise => { + const host: string = (req.query.host as string) || "local"; logger.info(`Fetching containers from host: ${host}`); try { const docker = getDockerClient(host); const containers = await docker.listContainers(); res.status(200).json(containers); - } catch (err) { + } catch (error: any) { logger.error( - `Error fetching containers from host: ${host} - ${err.message || "Unknown error"} - Full error: ${JSON.stringify(err, null, 2)}`, + `Error fetching containers from host: ${host} - ${error.message || "Unknown error"} - Full error: ${JSON.stringify(error, null, 2)}`, ); res.status(500).json({ - error: `Error fetching containers: ${err.message || "Unknown error"}`, + error: `Error fetching containers: ${error.message || "Unknown error"}`, }); } }; -const getContainerStats = async (containerID, containerHost) => { +const getContainerStats = async ( + containerID: string, + containerHost: string, + res: Response, +): Promise => { logger.info( `Fetching stats for container: ${containerID} from host: ${containerHost}`, ); @@ -33,7 +36,7 @@ const getContainerStats = async (containerID, containerHost) => { `Successfully fetched stats for container: ${containerID} from host: ${containerHost}`, ); res.status(200).json(stats); - } catch (error) { + } catch (error: any) { logger.error( `Error fetching stats for container: ${containerID} from host: ${containerHost} - ${error.message}`, ); @@ -43,7 +46,7 @@ const getContainerStats = async (containerID, containerHost) => { } }; -module.exports = { +export default { getContainers, getContainerStats, }; diff --git a/controllers/databaseMigration.js b/src/controllers/databaseMigration.ts similarity index 55% rename from controllers/databaseMigration.js rename to src/controllers/databaseMigration.ts index 263de07f..45f88d12 100644 --- a/controllers/databaseMigration.js +++ b/src/controllers/databaseMigration.ts @@ -1,13 +1,13 @@ -const db = require("../config/db"); -const logger = require("../utils/logger"); +import db from "../config/db"; +import logger from "../utils/logger"; -function clearOldEntries() { - const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1000; +function clearOldEntries(): void { + const twentyFourHoursAgo: number = Date.now() - 24 * 60 * 60 * 1000; db.run( `DELETE FROM data WHERE createdAt < ?`, [twentyFourHoursAgo], - (err) => { + (err: Error | null) => { if (err) { logger.error("Error deleting old entries:", err.message); throw new Error("Database cleanup failed"); @@ -17,4 +17,4 @@ function clearOldEntries() { ); } -module.exports = clearOldEntries; +export default clearOldEntries; diff --git a/src/controllers/fetchData.ts b/src/controllers/fetchData.ts new file mode 100644 index 00000000..be9fdc7e --- /dev/null +++ b/src/controllers/fetchData.ts @@ -0,0 +1,80 @@ +import db from "../config/db"; +import fetchAllContainers from "../utils/containerService"; +import logger from "../utils/logger"; +import fs from "fs"; +const filePath = "./src/data/states.json"; + +let previousState: { [key: string]: string } = {}; + +interface Container { + name: string; + id: string; + state: string; + hostName: string; +} + +interface AllContainerData { + [host: string]: Container[] | { error: string }; +} + +const fetchData = async (): Promise => { + try { + const allContainerData: AllContainerData = + (await fetchAllContainers()) || {}; + + if (process.env.OFFLINE === "true") { + logger.info("No new data inserted --- OFFLINE MODE"); + } else { + db.run( + `INSERT INTO data (info) VALUES (?)`, + [JSON.stringify(allContainerData)], + function (error) { + if (error) { + logger.error("Error inserting data:", error); + return; + } + logger.info(`Data inserted with ID: ${this.lastID}`); + }, + ); + } + + const containerStatus: AllContainerData = {}; + + Object.keys(allContainerData).forEach((host) => { + const containers = allContainerData[host]; + + // Handle if the containers are an array, otherwise handle the error + if (Array.isArray(containers)) { + containerStatus[host] = containers.map((container: Container) => ({ + name: container.name, + id: container.id, + state: container.state, + hostName: container.hostName, + })); + } else { + // If there's an error, handle it separately + containerStatus[host] = { error: "Error fetching containers" }; + } + }); + + if (fs.existsSync(filePath)) { + const fileData = fs.readFileSync(filePath, "utf8"); + previousState = fileData ? JSON.parse(fileData) : {}; + } + + // Compare previous and current state + if (JSON.stringify(previousState) !== JSON.stringify(containerStatus)) { + fs.writeFileSync(filePath, JSON.stringify(containerStatus, null, 2)); + logger.info(`Container states saved to ${filePath}`); + // TODO: Add logic + notification levels per service + } else { + logger.info("No state change detected, notifications not triggered."); + } + } catch (error: any) { + logger.error( + `Error fetching data: ${JSON.stringify(error)} \nStack trace: ${error.stack}`, + ); + } +}; + +export default fetchData; diff --git a/controllers/frontendConfiguration.js b/src/controllers/frontendConfiguration.ts similarity index 73% rename from controllers/frontendConfiguration.js rename to src/controllers/frontendConfiguration.ts index cdbee131..6a5a6911 100644 --- a/controllers/frontendConfiguration.js +++ b/src/controllers/frontendConfiguration.ts @@ -1,18 +1,17 @@ -const fs = require("fs"); -const path = require("path"); -const dataPath = path.join(__dirname, "../data/frontendConfiguration.json"); -const logger = require("../utils/logger"); -const expression = +import fs from "fs"; +import logger from "../utils/logger"; +const dataPath: string = "./src/data/frontendConfiguration.json"; +const expression: string = "https?://(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}([-a-zA-Z0-9()@:%_+.~#?&//=]*)"; const regex = new RegExp(expression); /////////////////////////////////////////////////////////////// // Hide Containers: -async function hideContainer(containerName) { +async function hideContainer(containerName: string) { try { let data = await readData(); const containerIndex = data.findIndex( - (container) => container.name === containerName, + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -22,17 +21,17 @@ async function hideContainer(containerName) { data.push({ name: containerName, hidden: true }); await saveData(data); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } } -async function unhideContainer(containerName) { +async function unhideContainer(containerName: string) { try { let data = await readData(); const containerIndex = data.findIndex( - (container) => container.name === containerName, + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -40,7 +39,7 @@ async function unhideContainer(containerName) { await saveData(data); cleanupData(); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } @@ -48,11 +47,11 @@ async function unhideContainer(containerName) { /////////////////////////////////////////////////////////////// // Tag containers -async function addTagToContainer(containerName, tag) { +async function addTagToContainer(containerName: string, tag: string) { try { let data = await readData(); const containerIndex = data.findIndex( - (container) => container.name === containerName, + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -65,27 +64,27 @@ async function addTagToContainer(containerName, tag) { data.push({ name: containerName, tags: [tag] }); await saveData(data); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } } -async function removeTagFromContainer(containerName, tag) { +async function removeTagFromContainer(containerName: string, tag: string) { try { let data = await readData(); const containerIndex = data.findIndex( - (container) => container.name === containerName, + (container: any) => container.name === containerName, ); if (containerIndex !== -1 && data[containerIndex].tags) { data[containerIndex].tags = data[containerIndex].tags.filter( - (t) => t !== tag, + (t: any) => t !== tag, ); await saveData(data); cleanupData(); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } @@ -93,11 +92,11 @@ async function removeTagFromContainer(containerName, tag) { /////////////////////////////////////////////////////////////// // Pin containers -async function pinContainer(containerName) { +async function pinContainer(containerName: string) { try { - let data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, + let data: any = await readData(); + const containerIndex: number = data.findIndex( + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -107,17 +106,17 @@ async function pinContainer(containerName) { data.push({ name: containerName, pinned: true }); await saveData(data); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } } -async function unpinContainer(containerName) { +async function unpinContainer(containerName: string) { try { let data = await readData(); const containerIndex = data.findIndex( - (container) => container.name === containerName, + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -125,7 +124,7 @@ async function unpinContainer(containerName) { await saveData(data); cleanupData(); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } @@ -133,12 +132,12 @@ async function unpinContainer(containerName) { /////////////////////////////////////////////////////////////// // Add/remove link from containers -async function setLink(containerName, link) { +async function setLink(containerName: string, link: string) { if (link.match(regex)) { try { - let data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, + let data: any = await readData(); + const containerIndex: any = data.findIndex( + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -148,7 +147,7 @@ async function setLink(containerName, link) { data.push({ name: containerName, link: `${link}` }); await saveData(data); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } @@ -158,11 +157,11 @@ async function setLink(containerName, link) { } } -async function removeLink(containerName) { +async function removeLink(containerName: string) { try { let data = await readData(); const containerIndex = data.findIndex( - (container) => container.name === containerName, + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -170,7 +169,7 @@ async function removeLink(containerName) { await saveData(data); cleanupData(); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } @@ -178,11 +177,11 @@ async function removeLink(containerName) { /////////////////////////////////////////////////////////////// // Add/remove icon from containers -async function setIcon(containerName, icon, custom) { +async function setIcon(containerName: string, icon: string, custom: boolean) { try { let data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, + const containerIndex: number = data.findIndex( + (container: any) => container.name === containerName, ); if (custom === true) { @@ -202,17 +201,17 @@ async function setIcon(containerName, icon, custom) { await saveData(data); } } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } } -async function removeIcon(containerName) { +async function removeIcon(containerName: string) { try { let data = await readData(); const containerIndex = data.findIndex( - (container) => container.name === containerName, + (container: any) => container.name === containerName, ); if (containerIndex !== -1) { @@ -220,7 +219,7 @@ async function removeIcon(containerName) { await saveData(data); cleanupData(); } - } catch (error) { + } catch (error: any) { logger.error(error); throw new Error(error); } @@ -232,7 +231,7 @@ async function readData() { try { const data = await fs.promises.readFile(dataPath, "utf-8"); return JSON.parse(data); - } catch (error) { + } catch (error: any) { console.error("readData"); if (error.code === "ENOENT") { await saveData([]); @@ -243,7 +242,7 @@ async function readData() { } } -async function saveData(data) { +async function saveData(data: any) { try { await fs.promises.writeFile( dataPath, @@ -251,7 +250,7 @@ async function saveData(data) { "utf-8", ); logger.info("Succesfully wrote to file"); - } catch (error) { + } catch (error: any) { logger.error(error); } } @@ -276,12 +275,12 @@ async function cleanupData() { } await saveData(cleanedData); - } catch (error) { + } catch (error: any) { logger.error(error); } } -module.exports = { +export { hideContainer, unhideContainer, addTagToContainer, diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts new file mode 100644 index 00000000..e8557574 --- /dev/null +++ b/src/controllers/highAvailability.ts @@ -0,0 +1,274 @@ +import logger from "../utils/logger"; +import fs from "fs"; +import chokidar from "chokidar"; +import path from "path"; +import { promisify } from "util"; + +const sleep = promisify(setTimeout); + +interface HighAvailabilityConfig { + active: boolean; + master: boolean; + nodes: string[]; +} + +interface Node { + ip: string; + id: number; +} + +interface HaNodeConfig { + master: string; +} + +interface NodeCache { + [nodes: string]: Node; +} + +const haMasterPath: string = "./src/data/highAvailability.json"; +const haNodePath: string = "./src/data/haNode.json"; +const nodeCachePath: string = "./src/data/nodeCache.json"; +const useUnsafeConnection = process.env.HA_UNSAFE || "false"; +const lockFilePath: string = "./src/data/ha.lock"; + +const configFiles: string[] = [ + "./src/data/dockerConfig.json", + "./src/data/states.json", + "./src/data/template.json", + "./src/data/frontendConfiguration.json", + "./src/data/nodeCache.json", + "./src/data/usePassword.txt", + "./src/data/password.json", +]; + +async function acquireLock(): Promise { + while (fs.existsSync(lockFilePath)) { + logger.warn("Lock file exists, waiting..."); + await sleep(100); + } + + try { + await fs.promises.writeFile(lockFilePath, "locked", { flag: "wx" }); + logger.debug("Lock acquired."); + } catch (error) { + logger.error(`Error acquiring lock: ${(error as Error).message}`); + throw new Error("Failed to acquire lock."); + } +} + +async function releaseLock(): Promise { + try { + if (fs.existsSync(lockFilePath)) { + await fs.promises.unlink(lockFilePath); + logger.debug("Lock released."); + } + } catch (error) { + logger.error(`Error releasing lock: ${(error as Error).message}`); + } +} + +async function writeConfig( + data: HighAvailabilityConfig | NodeCache | HaNodeConfig, + filePath: string, +): Promise { + await acquireLock(); + try { + logger.debug(`Writing ${filePath}`); + const dirPath: string = path.dirname(filePath); + await fs.promises.mkdir(dirPath, { recursive: true }); + + const jsonData = JSON.stringify(data, null, 2); + await fs.promises.writeFile(filePath, jsonData); + + logger.debug(`${filePath} has been written.`); + } catch (error) { + logger.error(`Error writing config: ${(error as Error).message}`); + } finally { + await releaseLock(); + } +} + +async function readConfig(): Promise { + await acquireLock(); + try { + logger.debug("Reading HA-Config"); + const data: HighAvailabilityConfig = JSON.parse( + fs.readFileSync(haMasterPath, "utf-8"), + ); + return data; + } catch (error: any) { + logger.error(`Error reading HA-Config: ${(error as Error).message}`); + return null; + } finally { + await releaseLock(); + } +} + +async function prepareFilesForSync(): Promise> { + const fileData: Record = {}; + try { + for (const filePath of configFiles) { + const content = await fs.promises.readFile(filePath, "utf-8"); + fileData[filePath] = content; + } + } catch (error) { + logger.error(`Error preparing files for sync: ${(error as Error).message}`); + } + return fileData; +} + +async function checkApiReachable(node: string): Promise { + let nodeUrl = + useUnsafeConnection === "true" + ? `http://${node}/api/status` + : `https://${node}/api/status`; + + try { + const response = await fetch(nodeUrl); + if (!response.ok) { + logger.error(`Failed to reach node ${node}. Status: ${response.status}`); + return false; + } + + const data = await response.json(); + if (data.ApiReachable) { + logger.info(`Node ${node} is reachable.`); + return true; + } else { + logger.error(`Node ${node} is not reachable. ApiReachable: false`); + return false; + } + } catch (error) { + logger.error(`Error reaching node ${node}: ${(error as Error).message}`); + return false; + } +} + +async function synchronizeFilesWithNodes(): Promise { + const haConfig = await readConfig(); + + if (!haConfig || !haConfig.master || haConfig.nodes.length === 0) { + logger.warn("No slave nodes to synchronize with."); + return; + } + + const files = await prepareFilesForSync(); + + for (const node of haConfig.nodes) { + if (!(await checkApiReachable(node))) { + logger.warn( + `Skipping file sync with ${node} due to connectivity issues.`, + ); + continue; // Skip synchronization if the node is unreachable + } + + let nodeUrl = + useUnsafeConnection == "true" + ? `http://${node}/ha/sync` + : `https://${node}/ha/sync`; + + logger.info(`Synchronizing files with node: ${node}`); + + const response = await fetch(nodeUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ files }), + }); + + if (response.ok) { + logger.info(`Files synchronized successfully with node: ${node}`); + } else { + logger.error( + `Failed to synchronize files with node ${node}. Status: ${response.status}`, + ); + } + } +} + +function monitorConfigFiles(): void { + const watcher = chokidar.watch(configFiles, { persistent: true }); + + watcher + .on("change", async (filePath) => { + logger.info(`File changed: ${filePath}. Initiating synchronization.`); + await synchronizeFilesWithNodes(); + }) + .on("error", (error) => { + logger.error(`Error watching files: ${(error as Error).message}`); + }); + + logger.info("Started monitoring configuration files for changes."); +} + +async function startMasterNode() { + if (process.env.HA_MASTER == "true") { + if (!process.env.HA_MASTER_IP) { + logger.error( + "Master's IP is not set, please set the HA_MASTER_IP variable (example: 10.0.0.4:9876)", + ); + } else { + const haNodeConfig: HaNodeConfig = { + master: "HA_MASTER_IP", + }; + const haConfig: HighAvailabilityConfig = { + active: true, + master: true, + nodes: process.env.HA_NODE + ? process.env.HA_NODE.split(",").map((node) => node.trim()) + : [], + }; + + const nodeCache: NodeCache = process.env.HA_NODE + ? process.env.HA_NODE.split(",").reduce((cache, node, index) => { + const [ip, id] = node.trim().split(":"); + if (ip && id) { + cache[`node${index + 1}`] = { ip, id: parseInt(id, 10) }; + } + return cache; + }, {} as NodeCache) + : {}; + + logger.debug("Writing HA-Config(s)"); + await writeConfig(haConfig, haMasterPath); + await writeConfig(haNodeConfig, haNodePath); + await writeConfig(nodeCache, nodeCachePath); + + logger.info("Running startup sync..."); + await synchronizeFilesWithNodes(); + logger.info("Watching config files in ./data"); + monitorConfigFiles(); + } + } else { + logger.info("This is a slave node"); + } +} + +async function ensureFileExists( + filePath: string, + content: string, +): Promise { + await acquireLock(); + try { + const dirPath = path.dirname(filePath); + await fs.promises.mkdir(dirPath, { recursive: true }); + await fs.promises.writeFile(filePath, content, { flag: "w" }); + logger.info(`File created/updated: ${filePath}`); + } catch (error) { + logger.error( + `Error creating/updating file ${filePath}: ${(error as Error).message}`, + ); + } finally { + await releaseLock(); + } +} + +export { + HighAvailabilityConfig, + writeConfig, + readConfig, + prepareFilesForSync, + synchronizeFilesWithNodes, + monitorConfigFiles, + startMasterNode, + ensureFileExists, +}; diff --git a/src/controllers/notificationController.ts b/src/controllers/notificationController.ts new file mode 100644 index 00000000..e34eecda --- /dev/null +++ b/src/controllers/notificationController.ts @@ -0,0 +1,62 @@ +import notify from "../utils/notifications/_notify"; +import logger from "../utils/logger"; + +const notificationTypes = { + discord: !!process.env.DISCORD_WEBHOOK_URL, + email: !!( + process.env.EMAIL_SENDER && + process.env.EMAIL_RECIPIENT && + process.env.EMAIL_PASSWORD + ), + pushbullet: !!process.env.PUSHBULLET_ACCESS_TOKEN, + pushover: !!(process.env.PUSHOVER_API_TOKEN && process.env.PUSHOVER_USER_KEY), + slack: !!process.env.SLACK_WEBHOOK_UR, + telegram: !!(process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_CHAT_ID), + whatsapp: !!(process.env.WHATSAPP_API_URL && process.env.WHATSAPP_RECIPIENT), + custom: !!process.env.CUSTOM_NOTIFICATION, + customList: process.env.CUSTOM_NOTIFICATION, +}; + +async function sendNotification(containerId: string) { + if (notificationTypes.discord) { + logger.debug(`Sending notification via discord (${containerId})`); + notify("discord", containerId); + } + if (notificationTypes.email) { + logger.debug(`Sending notification via E-Mail (${containerId})`); + notify("email", containerId); + } + if (notificationTypes.pushbullet) { + logger.debug(`Sending notification via Pushbullet (${containerId})`); + notify("pushbullet", containerId); + } + if (notificationTypes.pushover) { + logger.debug(`Sending notification via Pushover (${containerId})`); + notify("pushover", containerId); + } + if (notificationTypes.slack) { + logger.debug(`Sending notification via Slack (${containerId})`); + notify("slack", containerId); + } + if (notificationTypes.telegram) { + logger.debug(`Sending notification via Telegram (${containerId})`); + notify("slack", containerId); + } + if (notificationTypes.whatsapp) { + logger.debug(`Sending notification via Pushbullet (${containerId})`); + notify("whatsapp", containerId); + } + if (notificationTypes.custom) { + const elements: undefined | string[] = notificationTypes.customList + ? notificationTypes.customList.split(",") + : undefined; + if (elements) { + elements.forEach((element) => { + logger.debug(`Sending custom notification ${element} (${containerId})`); + notify(`custom/${element}`, containerId); + }); + } else { + logger.error("Error getting custom notifications"); + } + } +} diff --git a/src/controllers/proxy.ts b/src/controllers/proxy.ts new file mode 100644 index 00000000..681adef7 --- /dev/null +++ b/src/controllers/proxy.ts @@ -0,0 +1,14 @@ +import { Application } from "express"; +import logger from "../utils/logger"; + +export default function trustedProxies(app: Application) { + const trusted: string = process.env.TRUSTED_PROXYS || ""; + + if (!trusted) { + logger.warn( + "No trusted Proxy configured, if ran behind a proxy please configure it according to the docs", + ); + } else { + app.set("trust proxy", trusted); + } +} diff --git a/controllers/scheduler.js b/src/controllers/scheduler.ts similarity index 56% rename from controllers/scheduler.js rename to src/controllers/scheduler.ts index e19b17eb..763b67f9 100644 --- a/controllers/scheduler.js +++ b/src/controllers/scheduler.ts @@ -1,15 +1,19 @@ -const fetchData = require("./fetchData"); -const logger = require("../utils/logger"); -const db = require("../config/db"); +import fetchData from "./fetchData"; +import logger from "../utils/logger"; +import db from "../config/db"; const regex = /(\d{1,5})([smh])/g; let fetchInterval = 5 * 60 * 1000; // Fetch data every 5 minutes by default -let intervalId; +const cleanupInterval = 24 * 60 * 60 * 1000; // every 24hrs +let intervalId: NodeJS.Timeout; const scheduleFetch = () => { - fetchData().then(() => { + try { + fetchData(); cleanupOldEntries(); - }); + } catch (error: any) { + logger.error(`Error during scheduled fetch: ${error}`); + } intervalId = setInterval(() => { logger.info( @@ -18,18 +22,24 @@ const scheduleFetch = () => { fetchData(); }, fetchInterval); - cleanupIntervalId = setInterval( - () => { - cleanupOldEntries(); - }, - 24 * 60 * 60 * 1000, - ); + setInterval(() => { + cleanupOldEntries(); + }, cleanupInterval); logger.info(`Data fetching scheduled every ${fetchInterval / 1000} seconds.`); logger.info("Old entries cleanup scheduled every 24 hours."); + + // Additional 20-second interval to log process exit listeners, if any + setInterval(() => { + const exitListeners = process.listeners("exit"); + + if (exitListeners.length > 0) { + logger.info(`Exit listeners detected: ${exitListeners}`); + } + }, 20000); }; -const setFetchInterval = (newInterval) => { +const setFetchInterval = (newInterval: number) => { if (intervalId) { clearInterval(intervalId); logger.info("Cleared existing fetch interval."); @@ -39,8 +49,8 @@ const setFetchInterval = (newInterval) => { logger.info(`Fetch interval updated to ${fetchInterval / 1000} seconds.`); }; -const parseInterval = (interval) => { - const timeUnits = { +const parseInterval = (interval: string) => { + const timeUnits: { [key: string]: number } = { s: 1000, m: 60 * 1000, h: 60 * 60 * 1000, @@ -69,16 +79,11 @@ const cleanupOldEntries = async () => { Date.now() - 24 * 60 * 60 * 1000, ).toISOString(); try { - await db.run("DELETE FROM data WHERE timestamp < ?", twentyFourHoursAgo); + db.run("DELETE FROM data WHERE timestamp < ?", twentyFourHoursAgo, Error); logger.info("Old entries cleared from the database."); - } catch (error) { - logger.error(`Error clearing old entries: ${error.message}`); + } catch (Error: any) { + logger.error(`Error clearing old entries: ${Error.message}`); } }; -module.exports = { - scheduleFetch, - setFetchInterval, - parseInterval, - getCurrentSchedule, -}; +export { scheduleFetch, setFetchInterval, parseInterval, getCurrentSchedule }; diff --git a/middleware/usePassword.txt b/src/data/usePassword.txt similarity index 100% rename from middleware/usePassword.txt rename to src/data/usePassword.txt diff --git a/src/init.ts b/src/init.ts new file mode 100644 index 00000000..3979eb6f --- /dev/null +++ b/src/init.ts @@ -0,0 +1,47 @@ +import express, { Request, Response, NextFunction } from "express"; +import swaggerDocs from "./utils/swaggerDocs"; +import auth from "./routes/auth/routes"; +import data from "./routes/data/routes"; +import frontend from "./routes/frontendController/routes"; +import api from "./routes/getter/routes"; +import notificationService from "./routes/notifications/routes"; +import conf from "./routes/setter/routes"; +import authMiddleware from "./middleware/authMiddleware"; +import ha from "./routes/highavailability/routes"; +import trustedProxies from "./controllers/proxy"; +import { limiter } from "./middleware/rateLimiter"; +import { scheduleFetch } from "./controllers/scheduler"; +import cors from "cors"; +import { blockWhileLocked } from "./middleware/checkLock"; + +const initializeApp = (app: express.Application): void => { + app.use(cors()); + app.use(express.json()); + app.use("/api-docs", (req: Request, res: Response, next: NextFunction) => + next(), + ); + + swaggerDocs(app as any); + trustedProxies(app); // Configures proxies using CSV string + scheduleFetch(); + + app.use("/api", limiter, authMiddleware, blockWhileLocked, api); + app.use("/conf", limiter, authMiddleware, blockWhileLocked, conf); + app.use("/auth", limiter, authMiddleware, blockWhileLocked, auth); + app.use("/data", limiter, authMiddleware, blockWhileLocked, data); + app.use("/frontend", limiter, authMiddleware, blockWhileLocked, frontend); + app.use( + "/notification-service", + limiter, + authMiddleware, + blockWhileLocked, + notificationService, + ); + app.use("/ha", limiter, authMiddleware, ha); + + app.get("/", (req: Request, res: Response) => { + res.redirect("/api-docs"); + }); +}; + +export default initializeApp; diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts new file mode 100644 index 00000000..8caad081 --- /dev/null +++ b/src/middleware/authMiddleware.ts @@ -0,0 +1,52 @@ +import bcrypt from "bcrypt"; +import fs from "fs"; +import { Request, Response, NextFunction } from "express"; +import logger from "../utils/logger"; + +const passwordFile = "./src/data/password.json"; +const passwordBool = "./src/data/usePassword.txt"; + +async function authMiddleware( + req: Request, + res: Response, + next: NextFunction, +): Promise { + try { + const authStatusData = await fs.promises.readFile(passwordBool, "utf8"); + const isAuthEnabled = authStatusData.trim() === "true"; + + if (!isAuthEnabled) { + logger.warn("You are not using authentication, please enable it."); + logger.debug("Authentication disabled, skipping login process..."); + return next(); + } + + const providedPassword = req.headers["x-password"]; + if (!providedPassword) { + logger.error("Password required - Denied"); + res.status(401).json({ message: "Password required" }); + return; + } + + const passwordData = await fs.promises.readFile(passwordFile, "utf8"); + const storedData = JSON.parse(passwordData); + + const passwordMatch = await bcrypt.compare( + providedPassword as string, + storedData.hash, + ); + if (!passwordMatch) { + logger.error("Invalid Password - Denied access"); + res.status(401).json({ message: "Invalid password" }); + return; + } + + logger.debug("Authentication succesfull"); + next(); + } catch (error: any) { + logger.error("Error in authMiddleware:", error); + res.status(500).json({ message: "Internal server error" }); + } +} + +export default authMiddleware; diff --git a/src/middleware/checkLock.ts b/src/middleware/checkLock.ts new file mode 100644 index 00000000..747889dc --- /dev/null +++ b/src/middleware/checkLock.ts @@ -0,0 +1,19 @@ +import fs from "fs"; +import { Request, Response, NextFunction } from "express"; + +const lockFilePath = "./src/data/ha.lock"; + +export function blockWhileLocked( + req: Request, + res: Response, + next: NextFunction, +): void { + if (fs.existsSync(lockFilePath)) { + res.status(503).json({ + error: + "Service unavailable. The high-availability lock is currently active. Please try again later.", + }); + } else { + next(); + } +} diff --git a/middleware/rateLimiter.js b/src/middleware/rateLimiter.ts similarity index 100% rename from middleware/rateLimiter.js rename to src/middleware/rateLimiter.ts diff --git a/src/misc/createEnvFile.sh b/src/misc/createEnvFile.sh new file mode 100644 index 00000000..cbd8244d --- /dev/null +++ b/src/misc/createEnvFile.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Version +VERSION="$1" + +# Docker +if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then + RUNNING_IN_DOCKER="true" +else + RUNNING_IN_DOCKER="false" +fi +echo " +{ + \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", + \"HA_MASTER\": \"${HA_MASTER}\", + \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", + \"HA_NODE\": \"${HA_NODE}\", + \"HA_UNSAFE\": \"${HA_UNSAFE}\", + \"DISCORD_WEBHOOK_URL\": \"${DISCORD_WEBHOOK_URL}\", + \"EMAIL_SENDER\": \"${EMAIL_SENDER}\", + \"EMAIL_RECIPIENT\": \"${EMAIL_RECIPIENT}\", + \"EMAIL_PASSWORD\": \"${EMAIL_PASSWORD}\", + \"EMAIL_SERVICE\": \"${EMAIL_SERVICE}\", + \"PUSHBULLET_ACCESS_TOKEN\": \"${PUSHBULLET_ACCESS_TOKEN}\", + \"PUSHOVER_USER_KEY\": \"${PUSHOVER_USER_KEY}\", + \"PUSHOVER_API_TOKEN\": \"${PUSHOVER_API_TOKEN}\", + \"SLACK_WEBHOOK_URL\": \"${SLACK_WEBHOOK_URL}\", + \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", + \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", + \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", + \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\", + \"CUSTOM_NOTIFICATION\": \"${CUSTOM_NOTIFICATION}\" +} +" > /api/src/data/variables.conf diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt new file mode 100644 index 00000000..7e77f2c9 --- /dev/null +++ b/src/misc/dependencyGraphs/mermaid-all.txt @@ -0,0 +1,106 @@ +flowchart LR + +0["server.ts"] +subgraph 1["controllers"] +2["highAvailability.ts"] +3["scheduler.ts"] +6["fetchData.ts"] +K["frontendConfiguration.ts"] +end +subgraph 4["config"] +5["db.ts"] +19["swaggerConfig.ts"] +end +subgraph 7["utils"] +8["containerService.ts"] +9["dockerClient.ts"] +N["connectionChecker.ts"] +P["extractHostData.ts"] +Q["writeOfflineLog.ts"] +subgraph V["notifications"] +W["_notify.ts"] +X["discord.ts"] +Y["_template.ts"] +Z["email.ts"] +10["pushbullet.ts"] +11["pushover.ts"] +12["slack.ts"] +13["telegram.ts"] +14["whatsapp.ts"] +end +end +subgraph A["middleware"] +B["authMiddleware.ts"] +C["rateLimiter.ts"] +end +subgraph D["routes"] +subgraph E["auth"] +F["routes.ts"] +end +subgraph G["data"] +H["routes.ts"] +end +subgraph I["frontendController"] +J["routes.ts"] +end +subgraph L["getter"] +M["routes.ts"] +end +subgraph R["highavailability"] +S["routes.ts"] +end +subgraph T["notifications"] +U["routes.ts"] +end +subgraph 15["setter"] +16["routes.ts"] +end +end +O["net"] +subgraph 17["swagger"] +18["swaggerDocs.ts"] +end +0-->2 +0-->3 +0-->B +0-->C +0-->F +0-->H +0-->J +0-->M +0-->S +0-->U +0-->16 +0-->18 +3-->5 +3-->6 +6-->5 +6-->8 +8-->9 +H-->5 +J-->K +M-->3 +M-->N +M-->8 +M-->9 +M-->P +M-->Q +N-->O +S-->2 +U-->W +W-->X +W-->Z +W-->10 +W-->11 +W-->12 +W-->13 +W-->14 +X-->Y +Z-->Y +10-->Y +11-->Y +12-->Y +13-->Y +14-->Y +16-->3 +18-->19 diff --git a/src/misc/dependencyGraphs/mermaid-api.txt b/src/misc/dependencyGraphs/mermaid-api.txt new file mode 100644 index 00000000..e7c85cc8 --- /dev/null +++ b/src/misc/dependencyGraphs/mermaid-api.txt @@ -0,0 +1,32 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["getter"] +2["routes.ts"] +end +end +subgraph 3["controllers"] +4["scheduler.ts"] +7["fetchData.ts"] +end +subgraph 5["config"] +6["db.ts"] +end +subgraph 8["utils"] +9["containerService.ts"] +A["dockerClient.ts"] +B["connectionChecker.ts"] +C["extractHostData.ts"] +D["writeOfflineLog.ts"] +end +2-->4 +2-->B +2-->9 +2-->A +2-->C +2-->D +4-->6 +4-->7 +7-->6 +7-->9 +9-->A diff --git a/misc/dependencyGraphs/mermaid-auth.txt b/src/misc/dependencyGraphs/mermaid-auth.txt similarity index 80% rename from misc/dependencyGraphs/mermaid-auth.txt rename to src/misc/dependencyGraphs/mermaid-auth.txt index e7ab0669..aaeb683b 100644 --- a/misc/dependencyGraphs/mermaid-auth.txt +++ b/src/misc/dependencyGraphs/mermaid-auth.txt @@ -2,7 +2,7 @@ flowchart LR subgraph 0["routes"] subgraph 1["auth"] -2["routes.js"] +2["routes.ts"] end end diff --git a/src/misc/dependencyGraphs/mermaid-conf.txt b/src/misc/dependencyGraphs/mermaid-conf.txt new file mode 100644 index 00000000..ba9ca669 --- /dev/null +++ b/src/misc/dependencyGraphs/mermaid-conf.txt @@ -0,0 +1,24 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["setter"] +2["routes.ts"] +end +end +subgraph 3["controllers"] +4["scheduler.ts"] +7["fetchData.ts"] +end +subgraph 5["config"] +6["db.ts"] +end +subgraph 8["utils"] +9["containerService.ts"] +A["dockerClient.ts"] +end +2-->4 +4-->6 +4-->7 +7-->6 +7-->9 +9-->A diff --git a/misc/dependencyGraphs/mermaid-data.txt b/src/misc/dependencyGraphs/mermaid-data.txt similarity index 78% rename from misc/dependencyGraphs/mermaid-data.txt rename to src/misc/dependencyGraphs/mermaid-data.txt index e212edcb..107d46af 100644 --- a/misc/dependencyGraphs/mermaid-data.txt +++ b/src/misc/dependencyGraphs/mermaid-data.txt @@ -2,10 +2,10 @@ flowchart LR subgraph 0["routes"] subgraph 1["data"] -2["routes.js"] +2["routes.ts"] end end subgraph 3["config"] -4["db.js"] +4["db.ts"] end 2-->4 diff --git a/misc/dependencyGraphs/mermaid-frontend.txt b/src/misc/dependencyGraphs/mermaid-frontend.txt similarity index 71% rename from misc/dependencyGraphs/mermaid-frontend.txt rename to src/misc/dependencyGraphs/mermaid-frontend.txt index 35b4e61b..03340053 100644 --- a/misc/dependencyGraphs/mermaid-frontend.txt +++ b/src/misc/dependencyGraphs/mermaid-frontend.txt @@ -2,10 +2,10 @@ flowchart LR subgraph 0["routes"] subgraph 1["frontendController"] -2["routes.js"] +2["routes.ts"] end end subgraph 3["controllers"] -4["frontendConfiguration.js"] +4["frontendConfiguration.ts"] end 2-->4 diff --git a/src/misc/dependencyGraphs/mermaid-ha.txt b/src/misc/dependencyGraphs/mermaid-ha.txt new file mode 100644 index 00000000..ce156053 --- /dev/null +++ b/src/misc/dependencyGraphs/mermaid-ha.txt @@ -0,0 +1,11 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["highavailability"] +2["routes.ts"] +end +end +subgraph 3["controllers"] +4["highAvailability.ts"] +end +2-->4 diff --git a/src/misc/dependencyGraphs/mermaid-notificationService.txt b/src/misc/dependencyGraphs/mermaid-notificationService.txt new file mode 100644 index 00000000..cef6c2cd --- /dev/null +++ b/src/misc/dependencyGraphs/mermaid-notificationService.txt @@ -0,0 +1,35 @@ +flowchart LR + +subgraph 0["routes"] +subgraph 1["notifications"] +2["routes.ts"] +end +end +subgraph 3["utils"] +subgraph 4["notifications"] +5["_notify.ts"] +6["discord.ts"] +7["_template.ts"] +8["email.ts"] +9["pushbullet.ts"] +A["pushover.ts"] +B["slack.ts"] +C["telegram.ts"] +D["whatsapp.ts"] +end +end +2-->5 +5-->6 +5-->8 +5-->9 +5-->A +5-->B +5-->C +5-->D +6-->7 +8-->7 +9-->7 +A-->7 +B-->7 +C-->7 +D-->7 diff --git a/src/misc/entrypoint.sh b/src/misc/entrypoint.sh new file mode 100755 index 00000000..ff5cc617 --- /dev/null +++ b/src/misc/entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +VERSION="2.0.0" + +echo -e " +\033[1;32mWelcome to\033[0m + +\033[1;34m###### ###### #### ### ### #### ######### ###### #########\033[0m +\033[1;34m### ### ### ### ### ### ### ### ### ### ### ###\033[0m +\033[1;34m### ### ### ### ### ###### #### ### ### ### ###\033[0m +\033[1;34m### ### ### ### ### ### ### #### ### ############ ###\033[0m +\033[1;34m### ### ### ### ### ### ### #### ### ### ### ###\033[0m +\033[1;34m###### ###### #### ### ### #### ### ### ### ### \033[0m(\033[1;33mAPI - v${VERSION}\033[0m) + +\033[1;36mUseful links:\033[0m + +- Documentation: \033[1;32mhttps://outline.itsnik.de/s/dockstat\033[0m +- GitHub (Frontend): \033[1;32mhttps://github.com/its4nik/dockstat\033[0m +- GitHub (Backend): \033[1;32mhttps://github.com/its4nik/dockstatapi\033[0m +- API Documentation: \033[1;32mhttp://localhost:7000/api-docs\033[0m + +\033[1;35mSummary:\033[0m + +DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. + +" + +bash "./createEnvFile.sh" "$VERSION" + +exec node src/server.js diff --git a/src/misc/minifyDist.sh b/src/misc/minifyDist.sh new file mode 100644 index 00000000..0c256170 --- /dev/null +++ b/src/misc/minifyDist.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +dist="$(pwd)/dist" + +run_script() { + npx uglifyjs --no-annotations --in-situ "$1" > /dev/null + echo "✔️ Minified : $(basename "$1")" +} + +if [ -d "$dist" ]; then + echo "::: Dist directory exists." +else + echo "::: Dist does not exist... Running npx tsc" + npx tsc +fi + +max_jobs=$(nproc) +job_count=0 + +for file in $(find "$dist" -type f); do + run_script "$file" & + ((job_count++)) + + if ((job_count >= max_jobs)); then + wait + job_count=0 + fi +done + +wait + +echo + +if [[ $1 == "--build-only" ]]; then + exit 0 +fi + +node dist/server.js diff --git a/src/routes/auth/routes.ts b/src/routes/auth/routes.ts new file mode 100644 index 00000000..4af13884 --- /dev/null +++ b/src/routes/auth/routes.ts @@ -0,0 +1,174 @@ +import { Router, Request, Response } from "express"; +import bcrypt from "bcrypt"; +import fs from "fs/promises"; +import logger from "../../utils/logger"; +const passwordFile: string = "./src/data/password.json"; +const passwordBool: string = "./src/data/usePassword.txt"; +const saltRounds: number = 10; +const router: Router = Router(); + +let passwordData: { + hash: string; + salt: string; +}; + +async function authEnabled(): Promise { + let isAuthEnabled: boolean = false; + let data: string = ""; + try { + data = await fs.readFile(passwordBool, "utf8"); + isAuthEnabled = data.trim() === "true"; + return isAuthEnabled; + } catch (error: any) { + logger.error("Error reading file: ", error); + return isAuthEnabled; + } +} + +async function readPasswordFile() { + let data: string = ""; + try { + data = await fs.readFile(passwordFile, "utf8"); + return data; + } catch (error: any) { + logger.error("Could not read saved password: ", error); + return data; + } +} + +async function writePasswordFile(passwordData: string) { + try { + await fs.writeFile(passwordFile, passwordData); + setTrue(); + logger.debug("Authentication enabled"); + return "Authentication enabled"; + } catch (error: any) { + logger.error("Error writing password file:", error); + return error; + } +} + +async function setTrue() { + try { + await fs.writeFile(passwordBool, "true", "utf8"); + logger.info(`Enabled authentication`); + return; + } catch (error: any) { + logger.error("Error writing to the file:", error); + return; + } +} + +async function setFalse() { + try { + await fs.writeFile(passwordBool, "false", "utf8"); + logger.info(`Disabled authentication`); + return; + } catch (error: any) { + logger.error("Error writing to the file:", error); + return; + } +} + +/** + * @swagger + * /auth/enable: + * post: + * summary: Enable authentication by setting a password + * tags: [Authentication] + * parameters: + * - name: password + * in: query + * required: true + * responses: + * 200: + * description: Authentication enabled. + * 400: + * description: Password is required. + * 500: + * description: Error saving password. + */ +router.post("/enable", async (req: Request, res: Response): Promise => { + try { + const password = req.query.password as string; + + if (await authEnabled()) { + logger.error( + "Password Authentication is already enabled, please deactivate it first", + ); + res.status(401).json({ + message: + "Password Authentication is already enabled, please deactivate it first", + }); + return; + } + + if (!password) { + logger.error("Password is required"); + res.status(400).json({ message: "Password is required" }); + return; + } + + const salt = await bcrypt.genSalt(saltRounds); + const hash = await bcrypt.hash(password, salt); + + const passwordData = { hash, salt }; + writePasswordFile(JSON.stringify(passwordData)); + + res + .status(200) + .json({ message: "Password Authentication enabled successfully" }); + } catch (error) { + logger.error(`Error enabling password authentication: ${error}`); + res.status(500).json({ message: "An error occurred" }); + } +}); + +/** + * @swagger + * /auth/disable: + * post: + * summary: Disable authentication by providing the existing password + * tags: [Authentication] + * parameters: + * - name: password + * in: query + * required: true + * responses: + * 200: + * description: Authentication disabled. + * 400: + * description: Password is required. + * 401: + * description: Invalid password. + * 500: + * description: Error disabling authentication. + */ +router.post("/disable", async (req: Request, res: Response): Promise => { + try { + const password = req.query.password as string; + + if (!password) { + logger.error("Password is required!"); + res.status(400).json({ message: "Password is required" }); + return; + } + + const storedData = JSON.parse(await readPasswordFile()); + + const isPasswordValid = await bcrypt.compare(password, storedData.hash); + if (!isPasswordValid) { + logger.error("Invalid password"); + res.status(401).json({ message: "Invalid password" }); + return; + } + + await setFalse(); // Assuming this is an async function + res.status(200).json({ message: "Authentication disabled" }); + } catch (error) { + logger.error(`Error disabling authentication: ${error}`); + res.status(500).json({ message: "An error occurred" }); + } +}); + +export default router; diff --git a/src/routes/data/routes.ts b/src/routes/data/routes.ts new file mode 100644 index 00000000..0e9a6e36 --- /dev/null +++ b/src/routes/data/routes.ts @@ -0,0 +1,201 @@ +import express from "express"; +const router = express.Router(); +import db from "../../config/db"; +import logger from "../../utils/logger"; + +interface DataRow { + info: string; +} + +function formatRows(rows: DataRow[]): Record { + return rows.reduce( + (acc: Record, row, index: number): Record => { + acc[index] = JSON.parse(row.info); + return acc; + }, + {}, + ); +} + +/** + * @swagger + * /data/latest: + * get: + * summary: Retrieve the latest container statistics for a specific host + * tags: [Database queries] + * responses: + * 200: + * description: A JSON object containing the latest container statistics for the specified host. + * content: + * application/json: + * schema: + * type: object + * properties: + * Fin-2: + * type: array + * items: + * type: object + * properties: + * name: + * type: string + * description: The name of the container + * example: "Container A" + * id: + * type: string + * description: Unique identifier for the container + * example: "abcd1234" + * hostName: + * type: string + * description: Name of the host system running this container + * example: "Fin-2" + * state: + * type: string + * description: Current state of the container + * example: "running" + * cpu_usage: + * type: number + * description: CPU usage percentage for this container + * example: 30 + * mem_usage: + * type: number + * description: Memory usage in bytes + * example: 2097152 + * mem_limit: + * type: number + * description: Memory limit in bytes set for this container + * example: 8123764736 + * net_rx: + * type: number + * description: Total network received bytes since container start + * example: 151763111 + * net_tx: + * type: number + * description: Total network transmitted bytes since container start + * example: 7104386 + * current_net_rx: + * type: number + * description: Current received bytes in the recent period + * example: 1048576 + * current_net_tx: + * type: number + * description: Current transmitted bytes in the recent period + * example: 524288 + * networkMode: + * type: string + * description: Networking mode for the container + * example: "bridge" + */ +router.get("/latest", (req, res) => { + db.get( + "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", + (error, row: any) => { + if (error) { + logger.error("Error fetching latest data:", error.message); + return res.status(500).json({ error: "Internal server error" }); + } + + if (!row) { + logger.warn("No data available for /data/latest"); + return res.status(404).json({ error: "No data available" }); + } + + logger.debug("Fetching /data/latest"); + res.json(JSON.parse(row.info)); + }, + ); +}); + +/** + * @swagger + * /data/time/24h: + * get: + * summary: Retrieve container statistics entries from the last 24 hours + * tags: [Database queries] + * responses: + * 200: + * description: A numbered array of 'info' JSON objects from the last 24 hours. + * content: + * application/json: + * schema: + * type: object + * properties: + * 0: + * type: object + * description: Statistics for the first entry within 24 hours. + * properties: + * name: + * type: string + * example: "Container A" + * id: + * type: string + * example: "abcd1234" + * cpu_usage: + * type: number + * example: 30 + * mem_usage: + * type: number + * example: 2048 + * 1: + * type: object + * description: Statistics for the second entry within 24 hours. + * properties: + * name: + * type: string + * example: "Container B" + * id: + * type: string + * example: "efgh5678" + * cpu_usage: + * type: number + * example: 45 + * mem_usage: + * type: number + * example: 3072 + */ +router.get("/time/24h", (req, res) => { + const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + db.all( + "SELECT info FROM data WHERE timestamp >= ?", + [oneDayAgo], + (error, rows: DataRow[]) => { + if (error) { + logger.error("Error fetching data from last 24 hours:", error.message); + return res.status(500).json({ error: "Internal server error" }); + } + logger.debug("Fetching /data/time/24h"); + res.json(formatRows(rows)); + }, + ); +}); + +/** + * @swagger + * /data/clear: + * delete: + * summary: Clear all container statistics entries from the database + * tags: [Database queries] + * responses: + * 200: + * description: A message indicating whether the database was cleared successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: Success message upon database clearance + * example: "Database cleared successfully." + */ +router.delete("/clear", (req, res) => { + db.run("DELETE FROM data", (err) => { + if (err) { + logger.error("Error clearing the database:", err.message); + return res.status(500).json({ error: "Internal server error" }); + } + logger.debug("Database cleared successfully"); + res.json({ message: "Database cleared successfully" }); + }); +}); + +export default router; diff --git a/routes/frontendController/routes.js b/src/routes/frontendController/routes.ts similarity index 97% rename from routes/frontendController/routes.js rename to src/routes/frontendController/routes.ts index de08c7a5..fe5d8411 100644 --- a/routes/frontendController/routes.js +++ b/src/routes/frontendController/routes.ts @@ -1,6 +1,7 @@ -const express = require("express"); +import express from "express"; +import logger from "../../utils/logger"; const router = express.Router(); -const { +import { hideContainer, unhideContainer, addTagToContainer, @@ -11,7 +12,7 @@ const { removeLink, setIcon, removeIcon, -} = require("../../controllers/frontendConfiguration"); +} from "../../controllers/frontendConfiguration"; /* ____ ___ ____ _____ @@ -67,9 +68,9 @@ router.post("/show/:containerName", async (req, res) => { const { containerName } = req.params; try { await unhideContainer(containerName); - res.json({ success: true, message: "Container unhidden successfully." }); - } catch (error) { - res.status(500).json({ success: false, error: error.message }); + res.status(200).json({ message: "Container unhidden successfully." }); + } catch (error: any) { + res.status(500).json({ error: error.message }); } }); @@ -126,7 +127,7 @@ router.post("/tag/:containerName/:tag", async (req, res) => { try { await addTagToContainer(containerName, tag); res.json({ success: true, message: "Tag added successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); @@ -178,7 +179,7 @@ router.post("/pin/:containerName", async (req, res) => { try { await pinContainer(containerName); res.json({ success: true, message: "Container pinned successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); @@ -236,7 +237,7 @@ router.post("/add-link/:containerName/:link", async (req, res) => { try { await setLink(containerName, link); res.json({ success: true, message: "Link added successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); @@ -304,7 +305,7 @@ router.post( await setIcon(containerName, icon, custom); res.json({ success: true, message: "Icon added successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }, @@ -366,7 +367,7 @@ router.delete("/hide/:containerName", async (req, res) => { try { await hideContainer(target); res.json({ success: true, message: `Container, ${target}, hidden.` }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); @@ -424,7 +425,7 @@ router.delete("/remove-tag/:containerName/:tag", async (req, res) => { try { await removeTagFromContainer(containerName, tag); res.json({ success: true, message: "Tag removed successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); @@ -476,7 +477,7 @@ router.delete("/unpin/:containerName", async (req, res) => { try { await unpinContainer(containerName); res.json({ success: true, message: "Container unpinned successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); @@ -528,7 +529,7 @@ router.delete("/remove-link/:containerName", async (req, res) => { try { await removeLink(containerName); res.json({ success: true, message: "Link removed successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); @@ -580,9 +581,9 @@ router.delete("/remove-icon/:containerName", async (req, res) => { try { await removeIcon(containerName); res.json({ success: true, message: "Icon removed successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ success: false, error: error.message }); } }); -module.exports = router; +export default router; diff --git a/routes/getter/routes.js b/src/routes/getter/routes.ts similarity index 68% rename from routes/getter/routes.js rename to src/routes/getter/routes.ts index 6c5fbc0d..0f9883f9 100644 --- a/routes/getter/routes.js +++ b/src/routes/getter/routes.ts @@ -1,16 +1,15 @@ -const extractRelevantData = require("../../utils/extractHostData"); -const express = require("express"); -const router = express.Router(); -const { - writeOfflineLog, - readOfflineLog, -} = require("../../utils/writeOfflineLog"); -const { getDockerClient } = require("../../utils/dockerClient"); -const { fetchAllContainers } = require("../../utils/containerService"); -const { getCurrentSchedule } = require("../../controllers/scheduler"); -const logger = require("../../utils/logger"); -const path = require("path"); -const fs = require("fs"); +import extractRelevantData from "../../utils/extractHostData"; +import { Router, Request, Response } from "express"; +import { writeOfflineLog, readOfflineLog } from "../../utils/writeOfflineLog"; +import getDockerClient from "../../utils/dockerClient"; +import fetchAllContainers from "../../utils/containerService"; +import { getCurrentSchedule } from "../../controllers/scheduler"; +import logger from "../../utils/logger"; +import fs from "fs"; +import checkReachability from "../../utils/connectionChecker"; +const configPath = "./src/data/dockerConfig.json"; +const router = Router(); +const userConf = "./src/data/user.conf"; /** * @swagger @@ -32,12 +31,65 @@ const fs = require("fs"); * type: string * example: ["local", "remote1"] */ +router.get("/hosts", (req: Request, res: Response) => { + logger.info(`Fetching config: ${configPath}`); + try { + const rawData = fs.readFileSync(configPath, "utf-8"); + const config = JSON.parse(rawData); + + if (!config.hosts) { + throw new Error("No hosts defined in configuration."); + } -router.get("/hosts", (req, res) => { - const config = require("../../config/dockerConfig.json"); - const hosts = config.hosts.map((host) => host.name); - logger.info("Fetching all available Docker hosts"); - res.status(200).json({ hosts }); + const hosts = config.hosts.map((host: any) => host.name); + logger.debug("Fetching all available Docker hosts"); + res.status(200).json({ hosts }); + } catch (error: any) { + logger.error("Error fetching hosts: " + error.message); + res.status(500).json({ error: "Failed to fetch Docker hosts" }); + } +}); + +/** + * @swagger + * /api/system: + * get: + * summary: Retrieve system configuration details + * tags: [Misc] + * responses: + * 200: + * description: A JSON object containing the system configuration details. + * content: + * application/json: + * schema: + * type: object + * description: The parsed configuration details. + * 500: + * description: An error occurred while fetching the system configuration. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * description: Error message detailing the issue encountered. + */ +router.get("/system", (req: Request, res: Response) => { + logger.info(`Fetching ${userConf}`); + + try { + const rawData = fs.readFileSync(userConf, "utf8"); + const config = JSON.parse(rawData); + + if (!config) { + res.status(500).json({ error: `Error received empty ${userConf}` }); + } + res.status(200).json(config); + } catch (error: any) { + logger.error(`Could not fetch ${userConf}: ${error}`); + res.status(500).json({ error: `Failed to fetch ${userConf}` }); + } }); /** @@ -81,7 +133,7 @@ router.get("/hosts", (req, res) => { * type: string * description: Error message detailing the issue encountered. */ -router.get("/host/:hostName/stats", async (req, res) => { +router.get("/host/:hostName/stats", async (req: Request, res: Response) => { const hostName = req.params.hostName; logger.info(`Fetching stats for host: ${hostName}`); if (process.env.OFFLINE === "true") { @@ -96,7 +148,7 @@ router.get("/host/:hostName/stats", async (req, res) => { writeOfflineLog(JSON.stringify(relevantData)); res.status(200).json(relevantData); - } catch (error) { + } catch (error: any) { logger.error( `Error fetching stats for host: ${hostName} - ${error.message || "Unknown error"}`, ); @@ -180,12 +232,13 @@ router.get("/host/:hostName/stats", async (req, res) => { * type: string * description: Error message detailing the issue encountered. */ -router.get("/containers", async (req, res) => { +router.get("/containers", async (req: Request, res: Response) => { logger.info("Fetching all containers across all hosts"); try { const allContainerData = await fetchAllContainers(); + logger.debug("Fetched /api/containers"); res.status(200).json(allContainerData); - } catch (error) { + } catch (error: any) { logger.error(`Error fetching containers: ${error.message}`); res.status(500).json({ error: "Failed to fetch containers" }); } @@ -216,13 +269,13 @@ router.get("/containers", async (req, res) => { * type: string * description: Error message detailing the issue encountered. */ -router.get("/config", async (req, res) => { - const configPath = path.join(__dirname, "../../config/dockerConfig.json"); +router.get("/config", async (req: Request, res: Response) => { try { const rawData = fs.readFileSync(configPath); const jsonData = JSON.parse(rawData.toString()); + logger.debug("Fetching /api/config"); res.status(200).json(jsonData); - } catch (error) { + } catch (error: any) { logger.error("Error loading dockerConfig.json: " + error.message); res.status(500).json({ error: "Failed to load Docker configuration" }); } @@ -246,8 +299,9 @@ router.get("/config", async (req, res) => { * type: integer * description: Current fetch interval in seconds. */ -router.get("/current-schedule", (req, res) => { +router.get("/current-schedule", (req: Request, res: Response) => { const currentSchedule = getCurrentSchedule(); + logger.debug("Fetching current shedule"); res.json(currentSchedule); }); @@ -255,23 +309,39 @@ router.get("/current-schedule", (req, res) => { * @swagger * /api/status: * get: - * summary: Check server status + * summary: Check the DockStatAPI and docker socket status of each host * tags: [Misc] - * description: Returns a 200 status with an "up" message to indicate the server is up and running. Used for Healthchecks + * description: Returns the status of the backend and online components, indicating which nodes are reachable or offline. * responses: * 200: - * description: Server is running + * description: Server and backend status * content: * application/json: * schema: * type: object * properties: - * status: - * type: string - * example: "up" + * backendReachable: + * type: boolean + * example: true + * online: + * type: object + * properties: + * Host-1: + * type: boolean + * example: true + * Host-2: + * type: boolean + * example: false */ -router.get("/status", (req, res) => { - res.status(200).json({ status: "up" }); + +router.get("/status", async (req: Request, res: Response) => { + logger.debug("Fetching /api/status"); + try { + let jsonData = await checkReachability(); + res.status(200).json(jsonData); + } catch (error: any) { + logger.error(`Error while fetching data: ${error}`); + } }); /** @@ -316,19 +386,27 @@ router.get("/status", (req, res) => { * type: string * description: Error message */ -router.get("/frontend-config", (req, res) => { - const configPath = path.join( - __dirname, - "../../data/frontendConfiguration.json", - ); +router.get("/frontend-config", (req: Request, res: Response) => { + const configPath: string = "./src/data/frontendConfiguration.json"; + + fs.stat(configPath, (exists) => { + if (exists == null) { + logger.debug(`${configPath} exists, trying to read it`); + } else if (exists.code === "ENOENT") { + logger.warn(`${configPath} doesn't exist, trying to create it`); + fs.promises.writeFile(configPath, JSON.stringify([], null, 2), "utf-8"); + } + }); + try { const rawData = fs.readFileSync(configPath); const jsonData = JSON.parse(rawData.toString()); + res.status(200).json(jsonData); - } catch (error) { + } catch (error: any) { logger.error("Error loading frontendConfiguration.json: " + error.message); res.status(500).json({ error: "Failed to load Frontend configuration" }); } }); -module.exports = router; +export default router; diff --git a/src/routes/highavailability/routes.ts b/src/routes/highavailability/routes.ts new file mode 100644 index 00000000..bc4cb794 --- /dev/null +++ b/src/routes/highavailability/routes.ts @@ -0,0 +1,92 @@ +// File: /src/routes/ha/routes.ts +import { Router, Request, Response } from "express"; +import logger from "../../utils/logger"; +import { + readConfig, + synchronizeFilesWithNodes, + prepareFilesForSync, + HighAvailabilityConfig, + ensureFileExists, +} from "../../controllers/highAvailability"; + +interface SyncRequestBody { + files: Record; +} + +const router = Router(); + +/** + * @swagger + * /ha/config: + * get: + * summary: Retrieve the High Availability Config + * tags: [High Availability] + * responses: + * 200: + * description: A JSON object containing the config. + */ +router.get("/config", async (req: Request, res: Response) => { + logger.info("Getting the HA-Config"); + const data = await readConfig(); + res.status(200).json(data); +}); + +/** + * @swagger + * /ha/sync: + * post: + * summary: Synchronize configuration files from master node. + * tags: [High Availability] + * responses: + * 200: + * description: Files synchronized successfully. + */ +router.post( + "/sync", + async ( + req: Request<{}, {}, SyncRequestBody>, + res: Response, + ): Promise => { + try { + const { files } = req.body; + + if (!files || typeof files !== "object") { + const errorMsg = + "Invalid request: 'files' object is missing or invalid."; + logger.error(errorMsg); + res.status(400).json({ message: errorMsg }); + return; + } + + logger.info("Received synchronization request from master node."); + + for (const [filePath, content] of Object.entries(files)) { + await ensureFileExists(filePath, content); + } + + logger.info("Synchronization completed successfully."); + res.status(200).json({ message: "Synchronization completed." }); + } catch (error) { + logger.error(`Error during synchronization: ${(error as Error).message}`); + res.status(500).json({ message: "Synchronization failed." }); + } + }, +); + +/** + * @swagger + * /ha/prepare-sync: + * get: + * summary: Prepare files for synchronization. + * tags: [High Availability] + * responses: + * 200: + * description: A JSON object containing files to sync. + */ +router.get("/prepare-sync", async (req: Request, res: Response) => { + logger.info("Preparing files for synchronization."); + const fileData = await prepareFilesForSync(); + res.status(200).json(fileData); +}); + +export default router; diff --git a/routes/notifications/routes.js b/src/routes/notifications/routes.ts similarity index 73% rename from routes/notifications/routes.js rename to src/routes/notifications/routes.ts index 592ab638..262d48f3 100644 --- a/routes/notifications/routes.js +++ b/src/routes/notifications/routes.ts @@ -1,13 +1,26 @@ -const express = require("express"); -const router = express.Router(); -const logger = require("../../utils/logger"); -const path = require("path"); -const fs = require("fs"); -const notify = require("../../utils/notifications/_notify"); -const dataTemplate = path.join( - __dirname, - "../../utils/notifications/data/template.json", -); +import { Request, Response, Router } from "express"; +import logger from "../../utils/logger"; +import fs from "fs"; +import notify from "../../utils/notifications/_notify"; +const dataTemplate = "./src/data/template.json"; +const router = Router(); + +/////////// +// Will be moved! + +interface TemplateData { + text: string; +} + +function isTemplateData(data: any): data is TemplateData { + return ( + data !== null && typeof data === "object" && typeof data.text === "string" + ); +} + +// Will be moved +/////////// + /** * @swagger * /notification-service/get-template: @@ -39,7 +52,7 @@ const dataTemplate = path.join( * type: string * description: Error message */ -router.get("/get-template", (req, res) => { +router.get("/get-template", (req: Request, res: Response) => { fs.readFile(dataTemplate, "utf-8", (error, data) => { if (error) { logger.error("Errored opening:", error); @@ -84,23 +97,28 @@ router.get("/get-template", (req, res) => { * type: string * description: Error message */ -router.post("/set-template", (req, res) => { - const newData = req.body; +router.post("/set-template", (req: Request, res: Response): void => { + const newData: TemplateData = req.body; + + if (!isTemplateData(newData)) { + res.status(400).json({ + message: "Invalid input format. Expected JSON with a 'text' field.", + }); + return; + } - fs.writeFile( - dataTemplate, - JSON.stringify(newData, null, 2), - "utf-8", - (error) => { - if (error) { - logger.error("Errored writing to file:", error); - return res - .status(500) - .json({ message: `Error writing to file: ${error}` }); - } + fs.promises + .writeFile(dataTemplate, JSON.stringify(newData, null, 2), "utf-8") + .then(() => { + logger.info("Template updated successfully."); res.json({ message: "Template updated successfully." }); - }, - ); + }) + .catch((error) => { + logger.error("Error writing to file: " + error.message); + res + .status(500) + .json({ message: `Error writing to file: ${error.message}` }); + }); }); /** @@ -146,14 +164,14 @@ router.post("/set-template", (req, res) => { * message: * type: string */ -router.post("/test/:type/:containerId", async (req, res) => { +router.post("/test/:type/:containerId", async (req: Request, res: Response) => { const { type, containerId } = req.params; try { await notify(type, containerId); res.json({ success: true, message: `Sent test notification to ${type}` }); - } catch (error) { + } catch (error: any) { res.json({ success: false, message: `Errored: ${error}` }); } }); -module.exports = router; +export default router; diff --git a/src/routes/setter/routes.ts b/src/routes/setter/routes.ts new file mode 100644 index 00000000..fcffeef9 --- /dev/null +++ b/src/routes/setter/routes.ts @@ -0,0 +1,180 @@ +import { setFetchInterval, parseInterval } from "../../controllers/scheduler"; +import logger from "../../utils/logger"; +import { Router, Request, Response } from "express"; +import fs from "fs"; + +const router = Router(); +const configPath: string = "./src/data/dockerConfig.json"; + +interface Host { + name: string; + url: string; + port: string; +} + +interface DockerConfig { + hosts: Host[]; +} + +/** + * @swagger + * /conf/addHost: + * put: + * summary: Add a new host to the Docker configuration + * tags: [Configuration] + * parameters: + * - name: name + * in: query + * required: true + * description: The name of the new host. + * - name: url + * in: query + * required: true + * description: The URL of the new host. + * - name: port + * in: query + * required: true + * description: The port of the new host. + * responses: + * 200: + * description: Host added successfully. + * 400: + * description: Bad request, invalid input. + * 500: + * description: An error occurred while adding the host. + */ + +router.put( + "/addHost", + async ( + req: Request< + unknown, + unknown, + unknown, + { name: string; url: string; port: string } + >, + res: Response, + ): Promise => { + const { name, url, port } = req.query; + + if (!name || !url || !port) { + res.status(400).json({ error: "Name, Port, and URL are required." }); + return; + } + + try { + const config: DockerConfig = JSON.parse( + fs.readFileSync(configPath, "utf-8"), + ); + + if (config.hosts.some((host) => host.name === name)) { + res.status(400).json({ error: "Host already exists." }); + return; + } + + config.hosts.push({ name, url, port }); + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + logger.info(`Added new host: ${name}`); + res.status(200).json({ message: "Host added successfully." }); + } catch (error: unknown) { + const err = error as Error; + logger.error("Error adding host: " + err.message); + res.status(500).json({ error: "Failed to add host." }); + } + }, +); + +/** + * @swagger + * /conf/scheduler: + * put: + * summary: Set fetch interval for data fetching + * tags: [Configuration] + * parameters: + * - name: interval + * in: query + * required: true + * description: The new interval for fetching data, e.g., "6h 20m", "300s". + * responses: + * 200: + * description: Fetch interval set successfully. + * 400: + * description: Invalid interval format or out of range. + */ +router.put("/scheduler", (req: any, res: any) => { + const interval = req.query.interval as string; + + try { + const newInterval = parseInterval(interval); + + if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { + return res + .status(400) + .json({ error: "Interval must be between 5 minutes and 6 hours." }); + } + + setFetchInterval(newInterval); + res.json({ message: `Fetch interval set to ${interval}.` }); + } catch (error: unknown) { + const err = error as Error; + logger.error("Error setting fetch interval: " + err.message); + res.status(400).json({ error: "Invalid interval format." }); + } +}); + +/** + * @swagger + * /conf/removeHost: + * delete: + * summary: Remove a host from the Docker configuration + * tags: [Configuration] + * parameters: + * - name: hostName + * in: query + * required: true + * description: The name of the host to remove. + * responses: + * 200: + * description: Host removed successfully. + * 404: + * description: Host not found. + * 500: + * description: An error occurred while removing the host. + */ +router.delete("/removeHost", (req: Request, res: Response): void => { + const hostName = req.query.hostName as string; + + if (!hostName) { + res.status(400).json({ error: "Host name is required." }); + return; + } + + fs.promises + .readFile(configPath, "utf-8") + .then((rawData) => { + const config: DockerConfig = JSON.parse(rawData); + const hostIndex = config.hosts.findIndex( + (host) => host.name === hostName, + ); + + if (hostIndex === -1) { + res.status(404).json({ error: "Host not found." }); + return; + } + + config.hosts.splice(hostIndex, 1); + + return fs.promises + .writeFile(configPath, JSON.stringify(config, null, 2)) + .then(() => { + logger.info(`Removed host: ${hostName}`); + res.status(200).json({ message: "Host removed successfully." }); + }); + }) + .catch((error) => { + logger.error("Error removing host: " + (error as Error).message); + res.status(500).json({ error: "Failed to remove host." }); + }); +}); + +export default router; diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 00000000..4853204b --- /dev/null +++ b/src/server.ts @@ -0,0 +1,17 @@ +import express from "express"; +import logger from "./utils/logger"; +import initializeApp from "./init"; +import { startMasterNode } from "./controllers/highAvailability"; +import writeUserConf from "./config/hostsystem"; + +const app = express(); +const PORT: number = 9876; + +writeUserConf(); +initializeApp(app); + +app.listen(PORT, () => { + logger.info(`Server is running on http://localhost:${PORT}`); + logger.info(`Swagger docs available at http://localhost:${PORT}/api-docs`); + startMasterNode(); +}); diff --git a/src/utils/connectionChecker.ts b/src/utils/connectionChecker.ts new file mode 100644 index 00000000..c61ffebd --- /dev/null +++ b/src/utils/connectionChecker.ts @@ -0,0 +1,77 @@ +import * as fs from "fs"; +import * as net from "net"; +import logger from "../config/loggerConfig"; + +const filePath: string = "./src/data/dockerConfig.json"; + +interface Host { + name: string; + url: string; + port: string; +} + +interface StatusResponse { + ApiReachable: boolean; + online: { [key: string]: boolean }; +} + +async function checkHostStatus(hosts: Host[]): Promise { + const results: { [key: string]: boolean } = {}; + for (const host of hosts) { + const { name, url, port } = host; + + const isOnline = await checkPort(url, parseInt(port, 10)); + + results[name] = !!isOnline; + + if (results[name] == true) { + logger.debug(`${host.url}:${port} is online`); + } else { + logger.debug(`${host.url}:${port} is unreachable`); + } + } + + return { + ApiReachable: true, + online: results, + }; +} + +function checkPort(host: string, port: number): Promise { + return new Promise((resolve) => { + const socket = new net.Socket(); + socket.setTimeout(3000); + + socket.on("connect", () => { + socket.end(); + resolve(true); + }); + + socket.on("timeout", () => { + socket.destroy(); + resolve(false); + }); + + socket.on("error", () => { + socket.destroy(); + resolve(false); + }); + + socket.connect(port, host); + }); +} + +async function checkReachability(): Promise { + try { + const data = fs.readFileSync(filePath, "utf-8"); + const parsedData = JSON.parse(data); + const hosts: Host[] = parsedData.hosts; + return await checkHostStatus(hosts); + + } catch (error: any) { + logger.error(`Error reading file: ${error}`); + return undefined; + } +} + +export default checkReachability; diff --git a/src/utils/containerService.ts b/src/utils/containerService.ts new file mode 100644 index 00000000..afc035a1 --- /dev/null +++ b/src/utils/containerService.ts @@ -0,0 +1,134 @@ +import logger from "./logger"; +import { ContainerInfo, ContainerStats, ContainerInspectInfo } from "dockerode"; +import getDockerClient from "./dockerClient"; +import fs from "fs"; +const configPath = "./src/data/dockerConfig.json"; + +interface HostConfig { + name: string; + [key: string]: any; +} + +interface ContainerData { + name: string; + id: string; + hostName: string; + state: string; + cpu_usage: number; + mem_usage: number; + mem_limit: number; + net_rx: number; + net_tx: number; + current_net_rx: number; + current_net_tx: number; + networkMode: string; +} + +interface AllContainerData { + [hostName: string]: ContainerData[] | { error: string }; +} + +function loadConfig() { + try { + if (!fs.existsSync(configPath)) { + logger.warn(`Config file not found. Creating an empty file at ${configPath}`); + fs.writeFileSync(configPath, JSON.stringify({ "hosts": [] }, null, 2), "utf-8"); + } + + const configData = fs.readFileSync(configPath, "utf-8"); + logger.debug("Loaded " + configPath); + return JSON.parse(configData); + } catch (error: any) { + logger.error(`Failed to load config: ${error.message}`); + return null; + } +} + +async function fetchAllContainers(): Promise { + const config = loadConfig(); + if (!config || !config.hosts) { + logger.error("Invalid or missing host configuration."); + return {}; + } + + const allContainerData: AllContainerData = {}; + + for (const hostConfig of config.hosts as HostConfig[]) { + const hostName = hostConfig.name; + try { + const docker: any = getDockerClient(hostName); + logger.debug(`Now processing: ${hostName}`); + const containers: ContainerInfo[] = await docker.listContainers({ + all: true, + }); + + allContainerData[hostName] = await Promise.all( + containers.map(async (container) => { + try { + const containerInstance = docker.getContainer(container.Id); + const containerInfo: ContainerInspectInfo = + await containerInstance.inspect(); + const containerStats: ContainerStats = + await containerInstance.stats({ stream: false }); + + const cpuDelta = + containerStats.cpu_stats.cpu_usage.total_usage - + containerStats.precpu_stats.cpu_usage.total_usage; + const systemCpuDelta = + containerStats.cpu_stats.system_cpu_usage - + containerStats.precpu_stats.system_cpu_usage; + const cpuUsage = + systemCpuDelta > 0 + ? (cpuDelta / systemCpuDelta) * + containerStats.cpu_stats.online_cpus + : 0; + + return { + name: container.Names[0].replace("/", ""), + id: container.Id, + hostName, + state: container.State, + cpu_usage: cpuUsage * 1000000000, + mem_usage: containerStats.memory_stats.usage, + mem_limit: containerStats.memory_stats.limit, + net_rx: containerStats.networks?.eth0?.rx_bytes || 0, + net_tx: containerStats.networks?.eth0?.tx_bytes || 0, + current_net_rx: containerStats.networks?.eth0?.rx_bytes || 0, + current_net_tx: containerStats.networks?.eth0?.tx_bytes || 0, + networkMode: containerInfo.HostConfig.NetworkMode || "unknown", + }; + } catch (containerError: any) { + logger.error( + `Error fetching details for container ID: ${container.Id} on host: ${hostName} - ${containerError.message}`, + ); + return { + name: container.Names[0].replace("/", ""), + id: container.Id, + hostName, + state: container.State, + cpu_usage: 0, + mem_usage: 0, + mem_limit: 0, + net_rx: 0, + net_tx: 0, + current_net_rx: 0, + current_net_tx: 0, + networkMode: "unknown", + }; + } + }), + ); + } catch (error: any) { + logger.error( + `Error fetching containers for host: ${hostName} - ${error.message}. Stack: ${error.stack}`, + ); + allContainerData[hostName] = { + error: `Error fetching containers: ${error.message}`, + }; + } + } + + return allContainerData; +} + +export default fetchAllContainers; diff --git a/src/utils/createDependencyGraph.sh b/src/utils/createDependencyGraph.sh new file mode 100755 index 00000000..c8229992 --- /dev/null +++ b/src/utils/createDependencyGraph.sh @@ -0,0 +1,37 @@ +#!/bin/bash +cd src || exit 1 +TMP=$(mktemp) + +cat ./server.ts | grep "./routes" | awk '{print $2,$4}' > $TMP + +spawn_worker(){ + local line="$1" + local target_route="$(echo "$line" | cut -d '"' -f2).ts" + local route=$(echo "$line" | awk '{print $1}') + + echo -e "\nRoute: $route \n${target_route}" + + npx depcruise \ + -p cli-feedback \ + -T mermaid \ + -x "../node_modules|logger|.dependency-cruiser|path|fs|net" \ + -f ./misc/dependencyGraphs/mermaid-${route}.txt \ + ${target_route} || exit 1 +} + +while read line; do + spawn_worker "$line" & +done < <(cat $TMP) + +npx depcruise \ + -p cli-feedback \ + -T mermaid \ + -x "../node_modules|logger|.dependency-cruiser|path|fs" \ + -f ./misc/dependencyGraphs/mermaid-all.txt \ + ./server.ts || exit 1 + +wait + +echo -e "\n========\n\n DONE\n\n========" + +exit 0 diff --git a/src/utils/dockerClient.ts b/src/utils/dockerClient.ts new file mode 100644 index 00000000..4cb3f70c --- /dev/null +++ b/src/utils/dockerClient.ts @@ -0,0 +1,54 @@ +// src/utils/dockerClient.ts +import Docker from "dockerode"; +import fs from "fs"; +import logger from "./logger"; + +interface DockerHostConfig { + name: string; + url: string; + port?: number; +} + +interface DockerConfig { + hosts: DockerHostConfig[]; +} + +function loadDockerConfig(): DockerConfig { + const configPath = "./src/data/dockerConfig.json"; + try { + const rawData = fs.readFileSync(configPath, "utf-8"); + logger.debug("Refreshed DockerConfig.json"); + return JSON.parse(rawData) as DockerConfig; + } catch (error: any) { + logger.error( + "Error loading dockerConfig.json: " + (error as Error).message, + ); + throw new Error("Failed to load Docker configuration"); + } +} + +function createDockerClient(hostConfig: DockerHostConfig): Docker { + logger.info( + `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port || 2375}`, + ); + return new Docker({ + host: hostConfig.url, + port: hostConfig.port || 2375, + protocol: "http", + }); +} + +const getDockerClient = (hostName: string): Docker => { + logger.debug(`Getting Docker Client for ${hostName}`); + const config = loadDockerConfig(); + const hostConfig = config.hosts.find((host) => host.name === hostName); + + if (!hostConfig) { + const errorMsg = `Docker host ${hostName} not found in configuration`; + logger.error(errorMsg); + throw new Error(errorMsg); + } + return createDockerClient(hostConfig); +}; + +export default getDockerClient; diff --git a/src/utils/extractHostData.ts b/src/utils/extractHostData.ts new file mode 100644 index 00000000..b6192ea7 --- /dev/null +++ b/src/utils/extractHostData.ts @@ -0,0 +1,57 @@ +interface Component { + Name: string; + Version: string; +} + +interface JsonData { + hostName: string; + info: { + ID: string; + Containers: number; + ContainersRunning: number; + ContainersPaused: number; + ContainersStopped: number; + Images: number; + OperatingSystem: string; + KernelVersion: string; + Architecture: string; + MemTotal: number; + NCPU: number; + }; + version: { + Components: Component[]; + }; +} + +type ComponentMap = Record; + +// Export the function with type annotations +function extractRelevantData(jsonData: JsonData) { + return { + hostName: jsonData.hostName, + info: { + ID: jsonData.info.ID, + Containers: jsonData.info.Containers, + ContainersRunning: jsonData.info.ContainersRunning, + ContainersPaused: jsonData.info.ContainersPaused, + ContainersStopped: jsonData.info.ContainersStopped, + Images: jsonData.info.Images, + OperatingSystem: jsonData.info.OperatingSystem, + KernelVersion: jsonData.info.KernelVersion, + Architecture: jsonData.info.Architecture, + MemTotal: jsonData.info.MemTotal, + NCPU: jsonData.info.NCPU, + }, + version: { + Components: jsonData.version.Components.reduce( + (acc, component) => { + acc[component.Name] = component.Version; + return acc; + }, + {}, + ), + }, + }; +} + +export default extractRelevantData; diff --git a/utils/logger.js b/src/utils/logger.ts similarity index 52% rename from utils/logger.js rename to src/utils/logger.ts index 9d25e5d6..e69955a9 100644 --- a/utils/logger.js +++ b/src/utils/logger.ts @@ -1,7 +1,7 @@ -const winston = require("winston"); -const loggerConfig = require("../config/loggerConfig"); +import winston, { transport } from "winston"; +import loggerConfig from "../config/loggerConfig"; -const transports = [new winston.transports.Console()]; +const transports: transport[] = [new winston.transports.Console()]; transports.push( new winston.transports.File({ @@ -15,4 +15,4 @@ const logger = winston.createLogger({ transports, }); -module.exports = logger; +export default logger; diff --git a/src/utils/notifications/_notify.ts b/src/utils/notifications/_notify.ts new file mode 100644 index 00000000..018b3dce --- /dev/null +++ b/src/utils/notifications/_notify.ts @@ -0,0 +1,85 @@ +import logger from "../../utils/logger"; +import { telegramNotification } from "./telegram"; +import { slackNotification } from "./slack"; +import { discordNotification } from "./discord"; +import { emailNotification } from "./email"; +import { whatsappNotification } from "./whatsapp"; +import { pushbulletNotification } from "./pushbullet"; +import { pushoverNotification } from "./pushover"; +import path from "path"; + +async function loadCustomNotification(scriptPath: string, containerId: string) { + try { + const absolutePath = path.resolve(__dirname, "./custom", scriptPath); + const customModule = await import(absolutePath); + + if (typeof customModule.default !== "function") { + const errorMsg = `The custom notification script at ${scriptPath} does not export a default function.`; + logger.error(errorMsg); + throw new Error(errorMsg); + } + + logger.debug(`Executing custom notification script: ${scriptPath}`); + await customModule.default(containerId); + } catch (error: any) { + logger.error( + `Failed to execute custom notification script (${scriptPath}): ${error.message}`, + ); + throw error; + } +} + +async function notify(type: string, containerId: string) { + if (!containerId) { + logger.error("Container ID is required."); + throw new Error("Container ID is required."); + } + + if (type.startsWith("custom/")) { + const scriptName = type.split("/")[1]; + if (!scriptName) { + const errorMsg = "Custom notification script name is invalid."; + logger.error(errorMsg); + throw new Error(errorMsg); + } + await loadCustomNotification(`${scriptName}.js`, containerId); + return; + } + + switch (type) { + case "telegram": + logger.debug("Sending Telegram notification..."); + await telegramNotification(containerId); + break; + case "slack": + logger.debug("Sending Slack notification..."); + await slackNotification(containerId); + break; + case "discord": + logger.debug("Sending Discord notification..."); + await discordNotification(containerId); + break; + case "email": + logger.debug("Sending Email notification..."); + await emailNotification(containerId); + break; + case "whatsapp": + logger.debug("Sending WhatsApp notification..."); + await whatsappNotification(containerId); + break; + case "pushbullet": + logger.debug("Sending Pushbullet notification..."); + await pushbulletNotification(containerId); + break; + case "pushover": + logger.debug("Sending Pushover notification..."); + await pushoverNotification(containerId); + break; + default: + const errorMsg = "Unknown notification type."; + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + +export default notify; diff --git a/utils/notifications/data/template.js b/src/utils/notifications/_template.ts similarity index 51% rename from utils/notifications/data/template.js rename to src/utils/notifications/_template.ts index 9a090f61..ecc327e1 100644 --- a/utils/notifications/data/template.js +++ b/src/utils/notifications/_template.ts @@ -1,36 +1,41 @@ -const fs = require("fs"); -const path = require("path"); -const logger = require("../../logger"); +import fs from "fs"; +import logger from "../logger"; +const templatePath: string = "./src/data/template.json"; +const containersPath: string = "./src/data/states.json"; -const templatePath = path.join(__dirname, "template.json"); -const containersPath = path.join(__dirname, "../../../data/states.json"); +interface Template { + "text": string +} function getTemplate() { try { const data = fs.readFileSync(templatePath, "utf8"); return JSON.parse(data); - } catch (error) { - console.error("Failed to load template:", error); + } catch (error: any) { + logger.error("Failed to load template:", error); return null; } } -function setTemplate(newTemplate) { +function setTemplate(newTemplate: string) { try { fs.writeFileSync( templatePath, JSON.stringify(newTemplate, null, 2), "utf8", ); - logger.log("Template updated successfully"); - } catch (error) { + logger.debug("Template updated successfully"); + } catch (error: any) { logger.error("Failed to update template:", error); } } -function renderTemplate(containerId) { - const template = getTemplate(); - if (!template) return null; +function renderTemplate(containerId: string) { + const template: Template = getTemplate(); + if (!template) { + logger.error("Template is missing or not a string"); + return null; + } try { const data = fs.readFileSync(containersPath, "utf8"); @@ -38,12 +43,12 @@ function renderTemplate(containerId) { let containerData = null; for (const host in containers) { - containerData = containers[host].find((c) => c.id === containerId); + containerData = containers[host].find((c: any) => c.id === containerId); if (containerData) break; } if (!containerData) { - console.error(`Container with ID ${containerId} not found`); + logger.error(`Container with ID ${containerId} not found`); return null; } @@ -51,12 +56,13 @@ function renderTemplate(containerId) { return Object.keys(containerData).reduce( (text, key) => text.replace(new RegExp(`{{${key}}}`, "g"), containerData[key]), - template.message, + template.text, ); - } catch (error) { + } catch (error: any) { logger.error("Failed to load containers:", error); return null; } } -module.exports = { getTemplate, setTemplate, renderTemplate }; + +export { getTemplate, setTemplate, renderTemplate }; diff --git a/src/utils/notifications/discord.ts b/src/utils/notifications/discord.ts new file mode 100644 index 00000000..24aaf905 --- /dev/null +++ b/src/utils/notifications/discord.ts @@ -0,0 +1,55 @@ +import * as https from 'https'; +import logger from "../logger"; +import { renderTemplate } from "./_template"; + +const discord_webhook_url: string | undefined = process.env.DISCORD_WEBHOOK_URL; + +export async function discordNotification(containerId: string): Promise { + const discord_message: string | null = renderTemplate(containerId); + if (!discord_message) { + logger.error("Failed to create notification message."); + return; + } + + if (!discord_webhook_url) { + logger.error("Discord webhook URL is not set."); + return; + } + + const postData = JSON.stringify({ + content: discord_message, + }); + + const url = new URL(discord_webhook_url); + + const options = { + hostname: url.hostname, + path: url.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + logger.error(`Discord API error: ${data}`); + } + }); + }); + + req.on('error', (error) => { + logger.error("Error sending Discord message:", error); + }); + + req.write(postData); + req.end(); +} diff --git a/src/utils/notifications/email.ts b/src/utils/notifications/email.ts new file mode 100644 index 00000000..fbefbab6 --- /dev/null +++ b/src/utils/notifications/email.ts @@ -0,0 +1,46 @@ +import { SendMailOptions, createTransport } from "nodemailer"; +import logger from "../logger"; +import { renderTemplate } from "./_template"; + +const email_sender: string | undefined = process.env.EMAIL_SENDER; +const email_recipient: string | undefined = process.env.EMAIL_RECIPIENT; +const email_password: string | undefined = process.env.EMAIL_PASSWORD; +const email_service: string | undefined = process.env.EMAIL_SERVICE; + +export async function emailNotification(containerId: string) { + // Validate email configuration parameters + if (!email_sender || !email_recipient || !email_password || !email_service) { + logger.error( + "Email notification failed: Missing configuration parameters. " + + "Please ensure EMAIL_SENDER, EMAIL_RECIPIENT, EMAIL_PASSWORD, and EMAIL_SERVICE are set in environment variables.", + ); + return; + } + + const email_message: string | null = renderTemplate(containerId); + if (!email_message) { + logger.error("Failed to create notification message."); + return; + } + + const transporter = createTransport({ + service: email_service, + auth: { + user: email_sender, + pass: email_password, + }, + }); + + const mailOptions: SendMailOptions = { + from: email_sender, + to: email_recipient, + subject: "DockStat", + text: email_message, + }; + + try { + await transporter.sendMail(mailOptions); + } catch (error: any) { + logger.error("Error sending email:", error); + } +} diff --git a/src/utils/notifications/pushbullet.ts b/src/utils/notifications/pushbullet.ts new file mode 100644 index 00000000..f008e68c --- /dev/null +++ b/src/utils/notifications/pushbullet.ts @@ -0,0 +1,59 @@ +import * as https from "https"; +import logger from "../logger"; +import { renderTemplate } from "./_template"; + +const pushbullet_access_token: string | undefined = + process.env.PUSHBULLET_ACCESS_TOKEN; + +export async function pushbulletNotification( + containerId: string, +): Promise { + const pushbullet_message: string | null = renderTemplate(containerId); + if (!pushbullet_message) { + logger.error("Failed to create notification message."); + return; + } + + if (!pushbullet_access_token) { + logger.error("Pushbullet access token is not set."); + return; + } + + const postData = JSON.stringify({ + type: "note", + title: "Container Notification", + body: pushbullet_message, + }); + + const options = { + hostname: "api.pushbullet.com", + path: "/v2/pushes", + method: "POST", + headers: { + "Access-Token": pushbullet_access_token, + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(postData), + }, + }; + + const req = https.request(options, (res) => { + let data = ""; + + res.on("data", (chunk) => { + data += chunk; + }); + + res.on("end", () => { + if (res.statusCode !== 200) { + logger.error(`Pushbullet API error: ${data}`); + } + }); + }); + + req.on("error", (error) => { + logger.error("Error sending Pushbullet message:", error); + }); + + req.write(postData); + req.end(); +} diff --git a/src/utils/notifications/pushover.ts b/src/utils/notifications/pushover.ts new file mode 100644 index 00000000..847c3296 --- /dev/null +++ b/src/utils/notifications/pushover.ts @@ -0,0 +1,56 @@ +import * as https from 'https'; +import logger from "../logger"; +import { renderTemplate } from "./_template"; + +const pushover_user_key: string | undefined = process.env.PUSHOVER_USER_KEY; +const pushover_api_token: string | undefined = process.env.PUSHOVER_API_TOKEN; + +export async function pushoverNotification(containerId: string): Promise { + const pushover_message: string | null = renderTemplate(containerId); + if (!pushover_message) { + logger.error("Failed to create notification message."); + return; + } + + if (!pushover_api_token || !pushover_user_key) { + logger.error("Pushover API token or user key is not set."); + return; + } + + const postData = new URLSearchParams({ + token: pushover_api_token, + user: pushover_user_key, + message: pushover_message, + }).toString(); + + const options = { + hostname: 'api.pushover.net', + path: '/1/messages.json', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(postData), + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + logger.error(`Pushover API error: ${data}`); + } + }); + }); + + req.on('error', (error) => { + logger.error("Error sending Pushover message:", error); + }); + + req.write(postData); + req.end(); +} diff --git a/src/utils/notifications/slack.ts b/src/utils/notifications/slack.ts new file mode 100644 index 00000000..b0a8e0b4 --- /dev/null +++ b/src/utils/notifications/slack.ts @@ -0,0 +1,55 @@ +import * as https from 'https'; +import logger from "../logger"; +import { renderTemplate } from "./_template"; + +const slack_webhook_url: string | undefined = process.env.SLACK_WEBHOOK_URL; + +export async function slackNotification(containerId: string): Promise { + const slack_message: string | null = renderTemplate(containerId); + if (!slack_message) { + logger.error("Failed to create notification message."); + return; + } + + if (!slack_webhook_url) { + logger.error("Slack webhook URL is not set."); + return; + } + + const postData = JSON.stringify({ + text: slack_message, + }); + + const url = new URL(slack_webhook_url); + + const options = { + hostname: url.hostname, + path: url.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + logger.error(`Slack API error: ${data}`); + } + }); + }); + + req.on('error', (error) => { + logger.error("Error sending Slack message:", error); + }); + + req.write(postData); + req.end(); +} diff --git a/src/utils/notifications/telegram.ts b/src/utils/notifications/telegram.ts new file mode 100644 index 00000000..174a12e5 --- /dev/null +++ b/src/utils/notifications/telegram.ts @@ -0,0 +1,55 @@ +import * as https from 'https'; +import logger from "../logger"; +import { renderTemplate } from "./_template"; + +const telegram_bot_token: string | undefined = process.env.TELEGRAM_BOT_TOKEN; +const telegram_chat_id: string | undefined = process.env.TELEGRAM_CHAT_ID; + +export async function telegramNotification(containerId: string): Promise { + const telegram_message: string | null = renderTemplate(containerId); + if (!telegram_message) { + logger.error("Failed to create notification message."); + return; + } + + if (!telegram_bot_token || !telegram_chat_id) { + logger.error("Telegram bot token or chat ID is not set."); + return; + } + + const postData = JSON.stringify({ + chat_id: telegram_chat_id, + text: telegram_message, + }); + + const options = { + hostname: 'api.telegram.org', + path: `/bot${telegram_bot_token}/sendMessage`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + logger.error(`Telegram API error: ${data}`); + } + }); + }); + + req.on('error', (error) => { + logger.error("Error sending message:", error); + }); + + req.write(postData); + req.end(); +} diff --git a/src/utils/notifications/whatsapp.ts b/src/utils/notifications/whatsapp.ts new file mode 100644 index 00000000..178f6d53 --- /dev/null +++ b/src/utils/notifications/whatsapp.ts @@ -0,0 +1,57 @@ +import * as https from 'https'; +import logger from "../logger"; +import { renderTemplate } from "./_template"; + +const whatsapp_api_url: string | undefined = process.env.WHATSAPP_API_URL; +const whatsapp_recipient: string | undefined = process.env.WHATSAPP_RECIPIENT; + +export async function whatsappNotification(containerId: string): Promise { + const whatsapp_message: string | null = renderTemplate(containerId); + if (!whatsapp_message) { + logger.error("Failed to create notification message."); + return; + } + + if (!whatsapp_api_url || !whatsapp_recipient) { + logger.error("WhatsApp API URL or recipient is not set."); + return; + } + + const postData = JSON.stringify({ + to: whatsapp_recipient, + body: whatsapp_message, + }); + + const url = new URL(whatsapp_api_url); + + const options = { + hostname: url.hostname, + path: url.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + if (res.statusCode !== 200) { + logger.error(`WhatsApp API error: ${data}`); + } + }); + }); + + req.on('error', (error) => { + logger.error("Error sending WhatsApp message:", error); + }); + + req.write(postData); + req.end(); +} diff --git a/src/utils/removeUnusedDeps.sh b/src/utils/removeUnusedDeps.sh new file mode 100755 index 00000000..b5b68ebf --- /dev/null +++ b/src/utils/removeUnusedDeps.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +echo "Creating unused dependency list" + +TMP="$(npx depcheck --ignores @types/supports-color,ipaddr.js,dependency-cruiser,tsx,@types/bcrypt,@types/express,@types/express-handlebars,@types/node,ts-node --quiet --oneline | tail -n 1 | tr -d '\n')" + +lines=$(echo "$TMP" | tr -s ' ' '\n' | wc -l) + +if ((lines == 0)); then + echo "No unused dependencies." +else + echo + echo "Removing these unused dependencies:" + for entry in $TMP; do + echo "$entry" + done + echo +fi + + +read -n 1 -p "Delete unused dependencies? (y/n) " input +echo + +case $input in + Y|y) + COMMAND=$(echo "npm remove $TMP") + $COMMAND + exit 0 + ;; + *) + echo "Aborting" + exit 1 + ;; +esac + +exit 2 diff --git a/src/utils/swaggerDocs.ts b/src/utils/swaggerDocs.ts new file mode 100644 index 00000000..9a386ddd --- /dev/null +++ b/src/utils/swaggerDocs.ts @@ -0,0 +1,11 @@ +import swaggerUi from "swagger-ui-express"; +import swaggerJsdoc from "swagger-jsdoc"; +import swaggerConfig from "../config/swaggerConfig"; +import { Express } from "express"; + +const swaggerDocs = (app: Express) => { + const specs = swaggerJsdoc(swaggerConfig); + app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs)); +}; + +export default swaggerDocs; diff --git a/src/utils/writeOfflineLog.ts b/src/utils/writeOfflineLog.ts new file mode 100644 index 00000000..244f62e0 --- /dev/null +++ b/src/utils/writeOfflineLog.ts @@ -0,0 +1,26 @@ +import fs from "fs"; +import logger from "../utils/logger"; + +const LOG_FILE_PATH = "./logs/hostStats.json"; + +async function writeOfflineLog(message: string) { + try { + if (!fs.existsSync(LOG_FILE_PATH)) { + await fs.promises.writeFile(LOG_FILE_PATH, message); + } + } catch (error: any) { + logger.error("Error writing one time reference log: ", error); + } +} + +async function readOfflineLog() { + try { + const data = await fs.promises.readFile(LOG_FILE_PATH, "utf-8"); + logger.debug("Returning data:", data); + return data; + } catch (error: any) { + logger.error("Error reading offline log:", error); + } +} + +export { writeOfflineLog, readOfflineLog }; diff --git a/swagger/swaggerDocs.js b/swagger/swaggerDocs.js deleted file mode 100644 index 57193722..00000000 --- a/swagger/swaggerDocs.js +++ /dev/null @@ -1,10 +0,0 @@ -const swaggerUi = require("swagger-ui-express"); -const swaggerJsdoc = require("swagger-jsdoc"); -const swaggerConfig = require("../config/swaggerConfig"); - -const swaggerDocs = (app) => { - const specs = swaggerJsdoc(swaggerConfig); - app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs)); -}; - -module.exports = swaggerDocs; diff --git a/tests/main.spec.ts b/tests/main.spec.ts new file mode 100644 index 00000000..f9006426 --- /dev/null +++ b/tests/main.spec.ts @@ -0,0 +1,131 @@ +import { test, expect } from '@playwright/test'; +import ora from 'ora'; + +interface Route { + url: string; +} + +interface FrontendRoute { + url: string; + type: string; +} + +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +test('Swagger - Auth enable and disable', async ({ page }) => { + await page.goto('http://localhost:9876/api-docs/'); + await page.getByLabel('post /auth/enable').click(); + await page.getByRole('button', { name: 'Try it out' }).click(); + await page.getByPlaceholder('password').click(); + await page.getByPlaceholder('password').fill('1'); + await page.getByRole('button', { name: 'Execute' }).click(); + await page.getByRole('button', { name: 'Authorize' }).click(); + await page.getByLabel('Value:').click(); + await page.getByLabel('Value:').fill('1'); + await page.getByLabel('Apply credentials').click(); + await page.getByRole('button', { name: 'Close' }).click(); + await page.getByLabel('post /auth/disable').click(); + await page.getByRole('button', { name: 'Try it out' }).click(); + await page.getByRole('row', { name: 'password *required (query)', exact: true }).getByPlaceholder('password').click(); + await page.getByRole('row', { name: 'password *required (query)', exact: true }).getByPlaceholder('password').fill('1'); + await page.locator('#operations-Authentication-post_auth_disable').getByRole('button', { name: 'Execute' }).click(); +}); + +test('Return 200 status code', async ({ request }) => { + await sleep(5000); + const getRoutes: Route[] = [ + { url: 'http://localhost:9876/data/latest' }, + { url: 'http://localhost:9876/data/time/24h' }, + { url: 'http://localhost:9876/api/hosts' }, + { url: 'http://localhost:9876/api/host/Fin-2/stats' }, + { url: 'http://localhost:9876/api/containers' }, + { url: 'http://localhost:9876/api/config' }, + { url: 'http://localhost:9876/api/current-schedule' }, + { url: 'http://localhost:9876/api/frontend-config' }, + { url: 'http://localhost:9876/api/status' }, + { url: 'http://localhost:9876/ha/config' }, + { url: 'http://localhost:9876/ha/prepare-sync' }, + { url: 'http://localhost:9876/notification-service/get-template' } + ]; + + for (const { url } of getRoutes) { + const spinner = ora(`Checking: ${url}`).start(); + const response = await request.get(`${url}`); + await sleep(1000); + if (response.status() === 200) { + spinner.succeed(`Checked: ${url}`); + } else { + spinner.fail(`Failed: ${url}`); + } + expect(response.status()).toBe(200); + } + + const putRoutes: Route[] = [ + { url: 'http://localhost:9876/conf/addHost?name=test&url=localhost&port=2375' }, + { url: 'http://localhost:9876/conf/scheduler?interval=300s' } + ]; + + for (const { url } of putRoutes) { + const spinner = ora(`Checking: ${url}`).start(); + const response = await request.put(`${url}`); + await sleep(1000); + if (response.status() === 200) { + spinner.succeed(`Checked: ${url}`); + } else { + spinner.fail(`Failed: ${url}`); + } + expect(response.status()).toBe(200); + } + + const data = { text: "{{name}} ({{id}}) on {{hostName}} is {{state}}." }; + + const spinner = ora('Checking: http://localhost:9876/notification-service/set-template').start(); + const response = await request.post('http://localhost:9876/notification-service/set-template', { data }); + await sleep(1000); + if (response.status() === 200) { + spinner.succeed('Checked: http://localhost:9876/notification-service/set-template'); + } else { + spinner.fail('Failed: http://localhost:9876/notification-service/set-template'); + } + expect(response.status()).toBe(200); + + // Remove test host: + const deleteSpinner = ora('Removing test host').start(); + await request.delete('http://localhost:9876/conf/removeHost?hostName=test'); + await sleep(1000); + deleteSpinner.succeed('Removed test host'); + + const frontendRoutes: FrontendRoute[] = [ + { url: 'http://localhost:9876/frontend/tag/test/test', type: "post" }, + { url: 'http://localhost:9876/frontend/pin/test', type: "post" }, + { url: 'http://localhost:9876/frontend/add-link/test/https%3A%2F%2Fexample.com', type: "post" }, + { url: 'http://localhost:9876/frontend/add-icon/test/test.png/true', type: "post" }, + { url: 'http://localhost:9876/frontend/hide/test', type: "delete" }, + { url: 'http://localhost:9876/frontend/remove-tag/test/test', type: "delete" }, + { url: 'http://localhost:9876/frontend/remove-link/test', type: "delete" }, + { url: 'http://localhost:9876/frontend/show/test', type: "post" }, + { url: 'http://localhost:9876/frontend/remove-icon/test', type: "delete" }, + { url: 'http://localhost:9876/frontend/unpin/test', type: "delete" } + ]; + + for (const { url, type } of frontendRoutes) { + const spinner = ora(`Checking: ${url}`).start(); + let response; + if (type === "post") { + response = await request.post(`${url}`); + } else if (type === "put") { + response = await request.put(`${url}`); + } else if (type === "delete") { + response = await request.delete(`${url}`); + } else { + throw new Error(`Unsupported request type: ${type}`); + } + await sleep(1000); + if (response.status() === 200) { + spinner.succeed(`Checked: ${url}`); + } else { + spinner.fail(`Failed: ${url}`); + } + expect(response.status()).toBe(200); + } +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..8fc3c320 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "outDir": "dist/src", + "module": "CommonJS", + "moduleResolution": "node", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true + }, + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Recommended", + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "**/*.spec.ts" + ] +} \ No newline at end of file diff --git a/utils/containerService.js b/utils/containerService.js deleted file mode 100644 index eb078a55..00000000 --- a/utils/containerService.js +++ /dev/null @@ -1,63 +0,0 @@ -const config = require("../config/dockerConfig.json"); -const logger = require("./logger"); -const { getDockerClient } = require("./dockerClient"); - -async function fetchAllContainers() { - const allContainerData = {}; - - for (const hostConfig of config.hosts) { - const hostName = hostConfig.name; - try { - const docker = getDockerClient(hostName); - const containers = await docker.listContainers({ all: true }); - - allContainerData[hostName] = await Promise.all( - containers.map(async (container) => { - const containerInfo = await docker - .getContainer(container.Id) - .inspect(); - const containerStats = await docker - .getContainer(container.Id) - .stats({ stream: false }); - const cpuDelta = - containerStats.cpu_stats.cpu_usage.total_usage - - containerStats.precpu_stats.cpu_usage.total_usage; - const systemCpuDelta = - containerStats.cpu_stats.system_cpu_usage - - containerStats.precpu_stats.system_cpu_usage; - const cpuUsage = - systemCpuDelta > 0 - ? (cpuDelta / systemCpuDelta) * - containerStats.cpu_stats.online_cpus - : 0; - - return { - name: container.Names[0].replace("/", ""), - id: container.Id, - hostName: hostName, - state: container.State, - cpu_usage: cpuUsage * 1000000000, - mem_usage: containerStats.memory_stats.usage, - mem_limit: containerStats.memory_stats.limit, - net_rx: containerStats.networks?.eth0?.rx_bytes || 0, - net_tx: containerStats.networks?.eth0?.tx_bytes || 0, - current_net_rx: containerStats.networks?.eth0?.rx_bytes || 0, - current_net_tx: containerStats.networks?.eth0?.tx_bytes || 0, - networkMode: containerInfo.HostConfig.NetworkMode, - }; - }), - ); - } catch (error) { - logger.error( - `Error fetching containers for host: ${hostName} - ${error.message}`, - ); - allContainerData[hostName] = { - error: `Error fetching containers: ${error.message}`, - }; - } - } - - return allContainerData; -} - -module.exports = { fetchAllContainers }; diff --git a/utils/createDependencyGraph.sh b/utils/createDependencyGraph.sh deleted file mode 100755 index 3e75de0a..00000000 --- a/utils/createDependencyGraph.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash - -TMP=$(mktemp) - -cat ./server.js | grep "./routes" | awk '{print $2,$4}' > $TMP - -while read line; do - target_route=$(echo "$line" | cut -d '"' -f2) - route=$(echo "$line" | awk '{print $1}') - - echo - echo "Route: $route" - echo ${target_route}.js - - - npx depcruise \ - -p cli-feedback \ - -T mermaid \ - -x "^node_modules|logger|.dependency-cruiser|path|fs" \ - -f ./misc/dependencyGraphs/mermaid-${route}.txt \ - ${target_route}.js - -done < <(cat $TMP) - -npx depcruise \ - -p cli-feedback \ - -T mermaid \ - -x "^node_modules|logger|.dependency-cruiser|path|fs" \ - -f ./misc/dependencyGraphs/mermaid-all.txt \ - ./ - -sleep 0.5 - -echo -e "\n========\n\n DONE\n\n========" diff --git a/utils/dockerClient.js b/utils/dockerClient.js deleted file mode 100644 index 3d691e50..00000000 --- a/utils/dockerClient.js +++ /dev/null @@ -1,45 +0,0 @@ -const Docker = require("dockerode"); -const fs = require("fs"); -const path = require("path"); -const logger = require("./logger"); - -// Function to dynamically load config on each request -function loadDockerConfig() { - const configPath = path.join(__dirname, "../config/dockerConfig.json"); - try { - const rawData = fs.readFileSync(configPath); - logger.debug("Refreshed DockerConfig.json"); - return JSON.parse(rawData); - } catch (error) { - logger.error("Error loading dockerConfig.json: " + error.message); - throw new Error("Failed to load Docker configuration"); - } -} - -// Function to create the Docker client using separate url and port -function createDockerClient(hostConfig) { - logger.info( - `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port}`, - ); - return new Docker({ - host: hostConfig.url, - port: hostConfig.port || 2375, // Use 2375 as default port for non-TLS - protocol: "http", // Ensure the use of http for non-TLS - }); -} - -// This function will get the Docker client based on the host configuration -const getDockerClient = (hostName) => { - logger.debug(`Getting Docker Client for ${hostName}`); - const config = loadDockerConfig(); // Dynamically load config - const hostConfig = config.hosts.find((host) => host.name === hostName); - - if (!hostConfig) { - const errorMsg = `Docker host ${hostName} not found in configuration`; - logger.error(errorMsg); - throw new Error(errorMsg); - } - return createDockerClient(hostConfig); -}; - -module.exports = { getDockerClient }; diff --git a/utils/extractHostData.js b/utils/extractHostData.js deleted file mode 100644 index 87db239f..00000000 --- a/utils/extractHostData.js +++ /dev/null @@ -1,26 +0,0 @@ -function extractRelevantData(jsonData) { - return { - hostName: jsonData.hostName, - info: { - ID: jsonData.info.ID, - Containers: jsonData.info.Containers, - ContainersRunning: jsonData.info.ContainersRunning, - ContainersPaused: jsonData.info.ContainersPaused, - ContainersStopped: jsonData.info.ContainersStopped, - Images: jsonData.info.Images, - OperatingSystem: jsonData.info.OperatingSystem, - KernelVersion: jsonData.info.KernelVersion, - Architecture: jsonData.info.Architecture, - MemTotal: jsonData.info.MemTotal, - NCPU: jsonData.info.NCPU, - }, - version: { - Components: jsonData.version.Components.reduce((acc, component) => { - acc[component.Name] = component.Version; - return acc; - }, {}), - }, - }; -} - -module.exports = extractRelevantData; diff --git a/utils/notifications/_notify.js b/utils/notifications/_notify.js deleted file mode 100644 index b4a96fda..00000000 --- a/utils/notifications/_notify.js +++ /dev/null @@ -1,59 +0,0 @@ -const logger = require("../../utils/logger"); - -const { telegramNotification } = require("./telegram"); -const { slackNotification } = require("./slack"); -const { discordNotification } = require("./discord"); -const { emailNotification } = require("./email"); -const { whatsappNotification } = require("./whatsapp"); -const { pushbulletNotification } = require("./pushbullet"); -const { pushoverNotification } = require("./pushover"); - -async function notify(type, containerId) { - if (!containerId) { - logger.error("Container ID is required."); - throw new Error("Container ID is required."); - } - - switch (type) { - case "telegram": - logger.debug("Testing Telegram notification..."); - await telegramNotification(containerId); - break; - case "slack": - logger.debug("Testing Slack notification..."); - await slackNotification(containerId); - break; - case "discord": - logger.debug("Testing Discord notification..."); - await discordNotification(containerId); - break; - case "email": - logger.debug("Testing Email notification..."); - await emailNotification(containerId); - break; - case "whatsapp": - logger.debug("Testing WhatsApp notification..."); - await whatsappNotification(containerId); - break; - case "pushbullet": - logger.debug("Testing Pushbullet notification..."); - await pushbulletNotification(containerId); - break; - case "pushover": - logger.debug("Testing Pushover notification..."); - await pushoverNotification(containerId); - break; - default: - const errorMsg = "Unknown notification type."; - logger.error(errorMsg); - throw new Error(errorMsg); - } -} - -if (require.main === module) { - const [type, containerId] = process.argv.slice(2); - notify(type, containerId); - console.log(`Testing ${type}, with: ${containerId}`); -} - -module.exports = notify; diff --git a/utils/notifications/data/template.json b/utils/notifications/data/template.json deleted file mode 100644 index 6a57d442..00000000 --- a/utils/notifications/data/template.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "text": "{{name}} ({{id}}) on {{host}} is {{state}}." -} \ No newline at end of file diff --git a/utils/notifications/discord.js b/utils/notifications/discord.js deleted file mode 100644 index c7bfe828..00000000 --- a/utils/notifications/discord.js +++ /dev/null @@ -1,27 +0,0 @@ -import fetch from "node-fetch"; -import logger from "../logger.js"; -import { renderTemplate } from "./data/template.js"; - -const discord_webhook_url = process.env.DISCORD_WEBHOOK_URL; - -export async function discordNotification(containerId) { - const discord_message = renderTemplate(containerId); - if (!discord_message) { - logger.error("Failed to create notification message."); - return; - } - - try { - await fetch(discord_webhook_url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - content: discord_message, - }), - }); - } catch (error) { - logger.error("Error sending Discord message:", error); - } -} diff --git a/utils/notifications/email.js b/utils/notifications/email.js deleted file mode 100644 index d7016795..00000000 --- a/utils/notifications/email.js +++ /dev/null @@ -1,36 +0,0 @@ -import nodemailer from "nodemailer"; -import logger from "../logger.js"; -import { renderTemplate } from "./data/template.js"; - -const email_sender = process.env.EMAIL_SENDER; -const email_recipient = process.env.EMAIL_RECIPIENT; -const email_password = process.env.EMAIL_PASSWORD; - -export async function emailNotification(containerId) { - const email_message = renderTemplate(containerId); - if (!email_message) { - logger.error("Failed to create notification message."); - return; - } - - const transporter = nodemailer.createTransport({ - service: "gmail", - auth: { - user: email_sender, - pass: email_password, - }, - }); - - const mailOptions = { - from: email_sender, - to: email_recipient, - subject: "Container Notification", - text: email_message, - }; - - try { - await transporter.sendMail(mailOptions); - } catch (error) { - logger.error("Error sending email:", error); - } -} diff --git a/utils/notifications/pushbullet.js b/utils/notifications/pushbullet.js deleted file mode 100644 index 442f44d0..00000000 --- a/utils/notifications/pushbullet.js +++ /dev/null @@ -1,30 +0,0 @@ -import fetch from "node-fetch"; -import logger from "../logger.js"; -import { renderTemplate } from "./data/template.js"; - -const pushbullet_access_token = process.env.PUSHBULLET_ACCESS_TOKEN; - -export async function pushbulletNotification(containerId) { - const pushbullet_message = renderTemplate(containerId); - if (!pushbullet_message) { - logger.error("Failed to create notification message."); - return; - } - - try { - await fetch("https://api.pushbullet.com/v2/pushes", { - method: "POST", - headers: { - "Access-Token": pushbullet_access_token, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - type: "note", - title: "Container Notification", - body: pushbullet_message, - }), - }); - } catch (error) { - logger.error("Error sending Pushbullet message:", error); - } -} diff --git a/utils/notifications/pushover.js b/utils/notifications/pushover.js deleted file mode 100644 index 592e7f09..00000000 --- a/utils/notifications/pushover.js +++ /dev/null @@ -1,30 +0,0 @@ -import fetch from "node-fetch"; -import logger from "../logger.js"; -import { renderTemplate } from "./data/template.js"; - -const pushover_user_key = process.env.PUSHOVER_USER_KEY; -const pushover_api_token = process.env.PUSHOVER_API_TOKEN; - -export async function pushoverNotification(containerId) { - const pushover_message = renderTemplate(containerId); - if (!pushover_message) { - logger.error("Failed to create notification message."); - return; - } - - try { - await fetch("https://api.pushover.net/1/messages.json", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - token: pushover_api_token, - user: pushover_user_key, - message: pushover_message, - }), - }); - } catch (error) { - logger.error("Error sending Pushover message:", error); - } -} diff --git a/utils/notifications/slack.js b/utils/notifications/slack.js deleted file mode 100644 index 2c1a67a2..00000000 --- a/utils/notifications/slack.js +++ /dev/null @@ -1,27 +0,0 @@ -import fetch from "node-fetch"; -import logger from "../logger.js"; -import { renderTemplate } from "./data/template.js"; - -const slack_webhook_url = process.env.SLACK_WEBHOOK_URL; - -export async function slackNotification(containerId) { - const slack_message = renderTemplate(containerId); - if (!slack_message) { - logger.error("Failed to create notification message."); - return; - } - - try { - await fetch(slack_webhook_url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - text: slack_message, - }), - }); - } catch (error) { - logger.error("Error sending Slack message:", error); - } -} diff --git a/utils/notifications/telegram.js b/utils/notifications/telegram.js deleted file mode 100644 index 5c79bdc8..00000000 --- a/utils/notifications/telegram.js +++ /dev/null @@ -1,32 +0,0 @@ -import fetch from "node-fetch"; -import logger from "../logger.js"; -import { renderTemplate } from "./data/template.js"; - -const telegram_bot_token = process.env.TELEGRAM_BOT_TOKEN; -const telegram_chat_id = process.env.TELEGRAM_CHAT_ID; - -export async function telegramNotification(containerId) { - const telegram_message = renderTemplate(containerId); - if (!telegram_message) { - logger.error("Failed to create notification message."); - return; - } - - try { - await fetch( - `https://api.telegram.org/bot${telegram_bot_token}/sendMessage`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - chat_id: telegram_chat_id, - text: telegram_message, - }), - }, - ); - } catch (error) { - logger.error("Error sending message:", error); - } -} diff --git a/utils/notifications/whatsapp.js b/utils/notifications/whatsapp.js deleted file mode 100644 index d714b0b6..00000000 --- a/utils/notifications/whatsapp.js +++ /dev/null @@ -1,29 +0,0 @@ -import fetch from "node-fetch"; -import logger from "../logger.js"; -import { renderTemplate } from "./data/template.js"; - -const whatsapp_api_url = process.env.WHATSAPP_API_URL; // e.g., Twilio or other API service -const whatsapp_recipient = process.env.WHATSAPP_RECIPIENT; - -export async function whatsappNotification(containerId) { - const whatsapp_message = renderTemplate(containerId); - if (!whatsapp_message) { - logger.error("Failed to create notification message."); - return; - } - - try { - await fetch(whatsapp_api_url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - to: whatsapp_recipient, - body: whatsapp_message, - }), - }); - } catch (error) { - logger.error("Error sending WhatsApp message:", error); - } -} diff --git a/utils/writeOfflineLog.js b/utils/writeOfflineLog.js deleted file mode 100644 index 4d26b1d5..00000000 --- a/utils/writeOfflineLog.js +++ /dev/null @@ -1,31 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const logger = require("../utils/logger"); - -const LOG_FILE_PATH = path.join(__dirname, "../logs/hostStats.json"); - -function writeOfflineLog(message) { - try { - if (!fs.existsSync(LOG_FILE_PATH)) { - fs.writeFileSync(LOG_FILE_PATH, message); - } - } catch (error) { - logger.error("Error writing one time reference log: ", error); - } -} - -function readOfflineLog() { - fs.readFile(LOG_FILE_PATH, "utf-8", (err, data) => { - if (err) { - logger.error("Error reading offline log:", err); - } - - logger.debug("Returning data:", data); - return data; - }); -} - -module.exports = { - writeOfflineLog, - readOfflineLog, -}; diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..1418f00e --- /dev/null +++ b/yarn.lock @@ -0,0 +1,3298 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@apidevtools/json-schema-ref-parser@^9.0.6": + version "9.1.2" + resolved "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz" + integrity sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.6" + call-me-maybe "^1.0.1" + js-yaml "^4.1.0" + +"@apidevtools/openapi-schemas@^2.0.4": + version "2.1.0" + resolved "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz" + integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== + +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz" + integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== + +"@apidevtools/swagger-parser@10.0.3": + version "10.0.3" + resolved "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz" + integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== + dependencies: + "@apidevtools/json-schema-ref-parser" "^9.0.6" + "@apidevtools/openapi-schemas" "^2.0.4" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + z-schema "^5.0.1" + +"@babel/generator@7.18.2": + version "7.18.2" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz" + integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw== + dependencies: + "@babel/types" "^7.18.2" + "@jridgewell/gen-mapping" "^0.3.0" + jsesc "^2.5.1" + +"@babel/helper-string-parser@^7.18.10": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.18.6": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/parser@7.18.4": + version "7.18.4" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz" + integrity sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow== + +"@babel/types@^7.18.2", "@babel/types@7.19.0": + version "7.19.0" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz" + integrity sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA== + dependencies: + "@babel/helper-string-parser" "^7.18.10" + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + +"@balena/dockerignore@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz" + integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q== + +"@colors/colors@^1.6.0", "@colors/colors@1.6.0": + version "1.6.0" + resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz" + integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@dabh/diagnostics@^2.0.2": + version "2.0.3" + resolved "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz" + integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + +"@esbuild/linux-x64@0.23.1": + version "0.23.1" + resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz" + integrity sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ== + +"@gar/promisify@^1.0.1": + version "1.1.3" + resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.5" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.24": + version "0.3.25" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + +"@mapbox/node-pre-gyp@^1.0.11": + version "1.0.11" + resolved "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz" + integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": + version "2.0.5" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@npmcli/fs@^1.0.0": + version "1.1.1" + resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz" + integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" + +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + +"@playwright/test@^1.49.0": + version "1.49.0" + resolved "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz" + integrity sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw== + dependencies: + playwright "1.49.0" + +"@scarf/scarf@=1.4.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz" + integrity sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ== + +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/bcrypt@^5.0.2": + version "5.0.2" + resolved "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz" + integrity sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ== + dependencies: + "@types/node" "*" + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cors@^2.8.17": + version "2.8.17" + resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + +"@types/docker-modem@*": + version "3.0.6" + resolved "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz" + integrity sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg== + dependencies: + "@types/node" "*" + "@types/ssh2" "*" + +"@types/dockerode@^3.3.31": + version "3.3.32" + resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.32.tgz" + integrity sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg== + dependencies: + "@types/docker-modem" "*" + "@types/node" "*" + "@types/ssh2" "*" + +"@types/express-handlebars@^5.3.1": + version "5.3.1" + resolved "https://registry.npmjs.org/@types/express-handlebars/-/express-handlebars-5.3.1.tgz" + integrity sha512-DSzaERLO4gHb8AqnrL58jzSDyT0yDdl6HqDc+bGz1Hf0nrG1FK30nHGzv8NBEGR8QV9eUGB/YaE0Qj3NjF7siw== + +"@types/express-serve-static-core@^5.0.0": + version "5.0.2" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz" + integrity sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*", "@types/express@^5.0.0": + version "5.0.0" + resolved "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz" + integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/json-schema@^7.0.6": + version "7.0.15" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node-fetch@^2.6.12": + version "2.6.12" + resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz" + integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + +"@types/node@*", "@types/node@^22.9.0": + version "22.10.1" + resolved "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz" + integrity sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ== + dependencies: + undici-types "~6.20.0" + +"@types/node@^18.11.18": + version "18.19.67" + resolved "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz" + integrity sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ== + dependencies: + undici-types "~5.26.4" + +"@types/nodemailer@^6.4.17": + version "6.4.17" + resolved "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz" + integrity sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww== + dependencies: + "@types/node" "*" + +"@types/qs@*": + version "6.9.17" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz" + integrity sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.7" + resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/ssh2@*": + version "1.15.1" + resolved "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz" + integrity sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA== + dependencies: + "@types/node" "^18.11.18" + +"@types/supports-color@^8.1.3": + version "8.1.3" + resolved "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz" + integrity sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg== + +"@types/swagger-jsdoc@^6.0.4": + version "6.0.4" + resolved "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz" + integrity sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ== + +"@types/swagger-ui-express@^4.1.7": + version "4.1.7" + resolved "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz" + integrity sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g== + dependencies: + "@types/express" "*" + "@types/serve-static" "*" + +"@types/triple-beam@^1.3.2": + version "1.3.5" + resolved "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz" + integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== + +abbrev@1: + version "1.1.1" + resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-jsx-walk@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz" + integrity sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-loose@^8.4.0: + version "8.4.0" + resolved "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz" + integrity sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ== + dependencies: + acorn "^8.11.0" + +acorn-walk@^8.1.1, acorn-walk@^8.3.4: + version "8.3.4" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.11.0, acorn@^8.14.0, acorn@^8.4.1: + version "8.14.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + +agent-base@^6.0.2, agent-base@6: + version "6.0.2" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agentkeepalive@^4.1.3: + version "4.5.0" + resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz" + integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== + dependencies: + humanize-ms "^1.2.1" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv@^8.17.1: + version "8.17.1" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +are-we-there-yet@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz" + integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +asn1@^0.2.6: + version "0.2.6" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +async@^3.2.3: + version "3.2.6" + resolved "https://registry.npmjs.org/async/-/async-3.2.6.tgz" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bcrypt-pbkdf@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + +bcrypt@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz" + integrity sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.11" + node-addon-api "^5.0.0" + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buildcheck@~0.0.6: + version "0.0.6" + resolved "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz" + integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cacache@^15.2.0: + version "15.3.0" + resolved "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== + dependencies: + "@npmcli/fs" "^1.0.0" + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" + +call-bind-apply-helpers@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz" + integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.7: + version "1.0.8" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + +chokidar@^3.5.2: + version "3.6.0" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz" + integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== + dependencies: + readdirp "^4.0.1" + +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz" + integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw== + dependencies: + restore-cursor "^5.0.0" + +cli-spinners@^2.9.2: + version "2.9.2" + resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +color-convert@^1.9.3: + version "1.9.3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@^1.0.0, color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.6.0: + version "1.9.1" + resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color-support@^1.1.2, color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +color@^3.1.3: + version "3.2.1" + resolved "https://registry.npmjs.org/color/-/color-3.2.1.tgz" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + +colorspace@1.1.x: + version "1.1.4" + resolved "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz" + integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== + dependencies: + color "^3.1.3" + text-hex "1.0.x" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^12.1.0: + version "12.1.0" + resolved "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + +commander@^9.4.1: + version "9.5.0" + resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + +commander@6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz" + integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +console-control-strings@^1.0.0, console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cpu-features@~0.0.10: + version "0.0.10" + resolved "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz" + integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA== + dependencies: + buildcheck "~0.0.6" + nan "^2.19.0" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + +debug@^4, debug@^4.1.1, debug@^4.3.3, debug@4: + version "4.4.0" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +dependency-cruiser@^16.5.0: + version "16.7.0" + resolved "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.7.0.tgz" + integrity sha512-522LLjHINl9r0RIZ8/6s6TqIHTuEJG3XDU2WPSm9dG0rvLUYVyQwE9ID31tDFs4OOyEhdOPaqAaAG1jRv/Zwbg== + dependencies: + acorn "^8.14.0" + acorn-jsx "^5.3.2" + acorn-jsx-walk "^2.0.0" + acorn-loose "^8.4.0" + acorn-walk "^8.3.4" + ajv "^8.17.1" + commander "^12.1.0" + enhanced-resolve "^5.17.1" + ignore "^6.0.2" + interpret "^3.1.1" + is-installed-globally "^1.0.0" + json5 "^2.2.3" + memoize "^10.0.0" + picocolors "^1.1.1" + picomatch "^4.0.2" + prompts "^2.4.2" + rechoir "^0.8.0" + safe-regex "^2.1.1" + semver "^7.6.3" + teamcity-service-messages "^0.1.14" + tsconfig-paths-webpack-plugin "^4.2.0" + watskeburt "^4.1.1" + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-libc@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +docker-modem@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz" + integrity sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg== + dependencies: + debug "^4.1.1" + readable-stream "^3.5.0" + split-ca "^1.0.1" + ssh2 "^1.15.0" + +dockerode@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz" + integrity sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w== + dependencies: + "@balena/dockerignore" "^1.0.2" + docker-modem "^5.0.3" + tar-fs "~2.0.1" + +doctrine@3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dunder-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz" + integrity sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-errors "^1.3.0" + gopd "^1.2.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +emoji-regex@^10.3.0: + version "10.4.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz" + integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +encoding@^0.1.0, encoding@^0.1.12: + version "0.1.13" + resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^5.17.1, enhanced-resolve@^5.7.0: + version "5.17.1" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +esbuild@~0.23.0: + version "0.23.1" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz" + integrity sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.23.1" + "@esbuild/android-arm" "0.23.1" + "@esbuild/android-arm64" "0.23.1" + "@esbuild/android-x64" "0.23.1" + "@esbuild/darwin-arm64" "0.23.1" + "@esbuild/darwin-x64" "0.23.1" + "@esbuild/freebsd-arm64" "0.23.1" + "@esbuild/freebsd-x64" "0.23.1" + "@esbuild/linux-arm" "0.23.1" + "@esbuild/linux-arm64" "0.23.1" + "@esbuild/linux-ia32" "0.23.1" + "@esbuild/linux-loong64" "0.23.1" + "@esbuild/linux-mips64el" "0.23.1" + "@esbuild/linux-ppc64" "0.23.1" + "@esbuild/linux-riscv64" "0.23.1" + "@esbuild/linux-s390x" "0.23.1" + "@esbuild/linux-x64" "0.23.1" + "@esbuild/netbsd-x64" "0.23.1" + "@esbuild/openbsd-arm64" "0.23.1" + "@esbuild/openbsd-x64" "0.23.1" + "@esbuild/sunos-x64" "0.23.1" + "@esbuild/win32-arm64" "0.23.1" + "@esbuild/win32-ia32" "0.23.1" + "@esbuild/win32-x64" "0.23.1" + +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + +express-rate-limit@^7.4.1: + version "7.4.1" + resolved "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz" + integrity sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg== + +express@^4.21.1, "express@>=4.0.0 || >=5.0.0-beta", "express@4 || 5 || ^5.0.0-beta.1": + version "4.21.2" + resolved "https://registry.npmjs.org/express/-/express-4.21.2.tgz" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.12" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9: + version "3.3.2" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-uri@^3.0.1: + version "3.0.3" + resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz" + integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== + +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + +fecha@^4.2.0: + version "4.2.3" + resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz" + integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== + +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + +form-data@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +from2@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz" + integrity sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g== + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.3" + console-control-strings "^1.1.0" + has-unicode "^2.0.1" + signal-exit "^3.0.7" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.5" + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-east-asian-width@^1.0.0: + version "1.3.0" + resolved "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz" + integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== + +get-intrinsic@^1.2.4: + version "1.2.5" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz" + integrity sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg== + dependencies: + call-bind-apply-helpers "^1.0.0" + dunder-proto "^1.0.0" + es-define-property "^1.0.1" + es-errors "^1.3.0" + function-bind "^1.1.2" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + +get-tsconfig@^4.7.5: + version "4.8.1" + resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz" + integrity sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg== + dependencies: + resolve-pkg-maps "^1.0.0" + +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz" + integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-directory@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz" + integrity sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q== + dependencies: + ini "4.1.1" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + +has@^1.0.3: + version "1.0.4" + resolved "https://registry.npmjs.org/has/-/has-1.0.4.tgz" + integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +http-cache-semantics@^4.1.0: + version "4.1.1" + resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +https@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/https/-/https-1.0.0.tgz" + integrity sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg== + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +ignore@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz" + integrity sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@2, inherits@2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +ini@4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz" + integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +into-stream@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz" + integrity sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA== + dependencies: + from2 "^2.3.0" + p-is-promise "^3.0.0" + +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + +ipaddr.js@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + +is-core-module@2.9.0: + version "2.9.0" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== + dependencies: + has "^1.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + 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== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz" + integrity sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ== + dependencies: + global-directory "^4.0.1" + is-path-inside "^4.0.0" + +is-interactive@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz" + integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ== + +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz" + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz" + integrity sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-unicode-supported@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz" + integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== + +is-unicode-supported@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz" + integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json5@^2.2.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.mergewith@^4.6.2: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + +log-symbols@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz" + integrity sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw== + dependencies: + chalk "^5.3.0" + is-unicode-supported "^1.3.0" + +logform@^2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz" + integrity sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ== + dependencies: + "@colors/colors" "1.6.0" + "@types/triple-beam" "^1.3.2" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^2.3.1" + triple-beam "^1.3.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +make-fetch-happen@^9.1.0: + version "9.1.0" + resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.2.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.2" + promise-retry "^2.0.1" + socks-proxy-agent "^6.0.0" + ssri "^8.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memoize@^10.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/memoize/-/memoize-10.0.0.tgz" + integrity sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA== + dependencies: + mimic-function "^5.0.0" + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.4: + version "4.0.8" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-function@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz" + integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-fetch@^1.3.2: + version "1.4.1" + resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: + version "3.3.6" + resolved "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +minizlib@^2.0.0, minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +ms@^2.0.0, ms@^2.1.1, ms@^2.1.3, ms@2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +multistream@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz" + integrity sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw== + dependencies: + once "^1.4.0" + readable-stream "^3.6.0" + +nan@^2.19.0, nan@^2.20.0: + version "2.22.0" + resolved "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz" + integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw== + +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + +negotiator@^0.6.2, negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-abi@^3.3.0: + version "3.71.0" + resolved "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz" + integrity sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw== + dependencies: + semver "^7.3.5" + +node-addon-api@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz" + integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== + +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + +node-fetch@^2.6.6: + version "2.7.0" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@^3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + +node-gyp@8.x: + version "8.4.1" + resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz" + integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^9.1.0" + nopt "^5.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" + +nodemailer@^6.9.16: + version "6.9.16" + resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz" + integrity sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ== + +nodemon@^3.1.7: + version "3.1.7" + resolved "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz" + integrity sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ== + dependencies: + chokidar "^3.5.2" + debug "^4" + ignore-by-default "^1.0.1" + minimatch "^3.1.2" + pstree.remy "^1.1.8" + semver "^7.5.3" + simple-update-notifier "^2.0.0" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + +npmlog@^6.0.0: + version "6.0.2" + resolved "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== + dependencies: + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.3" + set-blocking "^2.0.0" + +object-assign@^4, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.3" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz" + integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + +onetime@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz" + integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== + dependencies: + mimic-function "^5.0.0" + +openapi-types@>=7: + version "12.1.3" + resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz" + integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== + +ora@^8.1.1: + version "8.1.1" + resolved "https://registry.npmjs.org/ora/-/ora-8.1.1.tgz" + integrity sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw== + dependencies: + chalk "^5.3.0" + cli-cursor "^5.0.0" + cli-spinners "^2.9.2" + is-interactive "^2.0.0" + is-unicode-supported "^2.0.0" + log-symbols "^6.0.0" + stdin-discarder "^0.2.2" + string-width "^7.2.0" + strip-ansi "^7.1.0" + +p-is-promise@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz" + integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ== + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + +pkg-fetch@3.4.2: + version "3.4.2" + resolved "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.4.2.tgz" + integrity sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA== + dependencies: + chalk "^4.1.2" + fs-extra "^9.1.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.6" + progress "^2.0.3" + semver "^7.3.5" + tar-fs "^2.1.1" + yargs "^16.2.0" + +pkg@^5.8.1: + version "5.8.1" + resolved "https://registry.npmjs.org/pkg/-/pkg-5.8.1.tgz" + integrity sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA== + dependencies: + "@babel/generator" "7.18.2" + "@babel/parser" "7.18.4" + "@babel/types" "7.19.0" + chalk "^4.1.2" + fs-extra "^9.1.0" + globby "^11.1.0" + into-stream "^6.0.0" + is-core-module "2.9.0" + minimist "^1.2.6" + multistream "^4.1.0" + pkg-fetch "3.4.2" + prebuild-install "7.1.1" + resolve "^1.22.0" + stream-meter "^1.0.4" + +playwright-core@1.49.0: + version "1.49.0" + resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz" + integrity sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA== + +playwright@1.49.0: + version "1.49.0" + resolved "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz" + integrity sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A== + dependencies: + playwright-core "1.49.0" + optionalDependencies: + fsevents "2.3.2" + +prebuild-install@^7.1.1: + version "7.1.2" + resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz" + integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + +prebuild-install@7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz" + integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz" + integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + +prompts@^2.4.2: + version "2.4.2" + resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +pump@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz" + integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.0.0: + version "2.3.8" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^2.1.4: + version "2.3.8" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2: + version "3.6.2" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz" + integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +regexp-tree@~0.1.1: + version "0.1.27" + resolved "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + +resolve@^1.20.0, resolve@^1.22.0: + version "1.22.8" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +restore-cursor@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz" + integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== + dependencies: + onetime "^7.0.0" + signal-exit "^4.1.0" + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@^5.0.1, safe-buffer@~5.2.0, safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz" + integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A== + dependencies: + regexp-tree "~0.1.1" + +safe-stable-stringify@^2.3.1: + version "2.5.0" + resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz" + integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^6.0.0: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.5, semver@^7.5.3, semver@^7.6.3: + version "7.6.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.19.0: + version "0.19.0" + resolved "https://registry.npmjs.org/send/-/send-0.19.0.tgz" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.0, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + +simple-update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz" + integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + dependencies: + semver "^7.5.3" + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@^6.0.0: + version "6.2.1" + resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz" + integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== + dependencies: + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + +socks@^2.6.2: + version "2.8.3" + resolved "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + +split-ca@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz" + integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ== + +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + +sqlite3@^5.1.7: + version "5.1.7" + resolved "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz" + integrity sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog== + dependencies: + bindings "^1.5.0" + node-addon-api "^7.0.0" + prebuild-install "^7.1.1" + tar "^6.1.11" + optionalDependencies: + node-gyp "8.x" + +ssh2@^1.15.0: + version "1.16.0" + resolved "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz" + integrity sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg== + dependencies: + asn1 "^0.2.6" + bcrypt-pbkdf "^1.0.2" + optionalDependencies: + cpu-features "~0.0.10" + nan "^2.20.0" + +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz" + integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +stdin-discarder@^0.2.2: + version "0.2.2" + resolved "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz" + integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ== + +stream-meter@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz" + integrity sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ== + dependencies: + readable-stream "^2.1.4" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^7.2.0: + version "7.2.0" + resolved "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +swagger-jsdoc@^6.2.8: + version "6.2.8" + resolved "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz" + integrity sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ== + dependencies: + commander "6.2.0" + doctrine "3.0.0" + glob "7.1.6" + lodash.mergewith "^4.6.2" + swagger-parser "^10.0.3" + yaml "2.0.0-1" + +swagger-parser@^10.0.3: + version "10.0.3" + resolved "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz" + integrity sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg== + dependencies: + "@apidevtools/swagger-parser" "10.0.3" + +swagger-ui-dist@>=5.0.0: + version "5.18.2" + resolved "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz" + integrity sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw== + dependencies: + "@scarf/scarf" "=1.4.0" + +swagger-ui-express@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz" + integrity sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA== + dependencies: + swagger-ui-dist ">=5.0.0" + +tapable@^2.2.0, tapable@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +tar-fs@^2.0.0, tar-fs@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz" + integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.0.0" + +tar-fs@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.0.0, tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: + version "6.2.1" + resolved "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +teamcity-service-messages@^0.1.14: + version "0.1.14" + resolved "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz" + integrity sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w== + +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +touch@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz" + integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +triple-beam@^1.3.0: + version "1.4.1" + resolved "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz" + integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== + +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tsconfig-paths-webpack-plugin@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz" + integrity sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.7.0" + tapable "^2.2.1" + tsconfig-paths "^4.1.2" + +tsconfig-paths@^4.1.2: + version "4.2.0" + resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tsx@^4.19.2: + version "4.19.2" + resolved "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz" + integrity sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g== + dependencies: + esbuild "~0.23.0" + get-tsconfig "^4.7.5" + optionalDependencies: + fsevents "~2.3.3" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3: + version "0.14.5" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@>=2.7: + version "5.7.2" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz" + integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== + +uglify-js@^3.19.3: + version "3.19.3" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz" + integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== + +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@~1.0.0, unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +validator@^13.7.0: + version "13.12.0" + resolved "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz" + integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +watskeburt@^4.1.1: + version "4.2.2" + resolved "https://registry.npmjs.org/watskeburt/-/watskeburt-4.2.2.tgz" + integrity sha512-AOCg1UYxWpiHW1tUwqpJau8vzarZYTtzl2uu99UptBmbzx6kOzCGMfRLF6KIRX4PYekmryn89MzxlRNkL66YyA== + +web-streams-polyfill@^3.0.3: + version "3.3.3" + resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.2, wide-align@^1.1.5: + version "1.1.5" + resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + +winston-transport@^4.9.0: + version "4.9.0" + resolved "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz" + integrity sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A== + dependencies: + logform "^2.7.0" + readable-stream "^3.6.2" + triple-beam "^1.3.0" + +winston@^3.15.0: + version "3.17.0" + resolved "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz" + integrity sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw== + dependencies: + "@colors/colors" "^1.6.0" + "@dabh/diagnostics" "^2.0.2" + async "^3.2.3" + is-stream "^2.0.0" + logform "^2.7.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + safe-stable-stringify "^2.3.1" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.9.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@2.0.0-1: + version "2.0.0-1" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz" + integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +z-schema@^5.0.1: + version "5.0.5" + resolved "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz" + integrity sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q== + dependencies: + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + validator "^13.7.0" + optionalDependencies: + commander "^9.4.1" From 84c3e993a69d6accfc3b7d31966874cf6b792829 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 20 Dec 2024 22:06:47 +0100 Subject: [PATCH 018/369] Fix: Disabled image pushing for PRs running against dev branch --- .github/workflows/test-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-build.yaml b/.github/workflows/test-build.yaml index fb471834..8c805d46 100644 --- a/.github/workflows/test-build.yaml +++ b/.github/workflows/test-build.yaml @@ -52,7 +52,7 @@ jobs: uses: docker/build-push-action@v5 with: platforms: linux/amd64,linux/arm64 - push: true + push: false tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} cache-from: type=gha From 49b69867db068ee38a63f6152bae6b81e05b45bb Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 10:51:48 +0100 Subject: [PATCH 019/369] Fix: Dropping linux/arm/v7 due to workflow timeout (sorry guys) --- .github/workflows/build-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index afc8ffc1..09f9b0e9 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -39,7 +39,7 @@ jobs: - name: Build and push uses: docker/build-push-action@v5 with: - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64, push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} From a2618b8ea170c4ad2ec2c9a7af9ab327d3a88056 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 12:03:24 +0100 Subject: [PATCH 020/369] Chore: Add exiting log --- package-lock.json | 898 ---------------------- package.json | 1 - src/init.ts | 29 + src/misc/dependencyGraphs/mermaid-all.txt | 197 ++--- src/utils/removeUnusedDeps.sh | 2 +- yarn.lock | 470 +---------- 6 files changed, 150 insertions(+), 1447 deletions(-) diff --git a/package-lock.json b/package-lock.json index 641c0d3a..dcd2ac0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,6 @@ "dependency-cruiser": "^16.5.0", "nodemon": "^3.1.7", "ora": "^8.1.1", - "pkg": "^5.8.1", "ts-node": "^10.9.2", "tsx": "^4.19.2", "uglify-js": "^3.19.3" @@ -93,69 +92,6 @@ "openapi-types": ">=7" } }, - "node_modules/@babel/generator": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", - "integrity": "sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.2", - "@jridgewell/gen-mapping": "^0.3.0", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.18.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", - "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==", - "dev": true, - "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", - "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.18.10", - "@babel/helper-validator-identifier": "^7.18.6", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@balena/dockerignore": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", @@ -610,32 +546,6 @@ "license": "MIT", "optional": true }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -646,16 +556,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -720,44 +620,6 @@ } } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -1311,16 +1173,6 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -1343,16 +1195,6 @@ "dev": true, "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1682,18 +1524,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -1819,13 +1649,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2023,19 +1846,6 @@ "node": ">=0.3.1" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/docker-modem": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", @@ -2239,16 +2049,6 @@ "@esbuild/win32-x64": "0.23.1" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2365,23 +2165,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, "node_modules/fast-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", @@ -2389,16 +2172,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -2531,72 +2304,12 @@ "node": ">= 0.6" } }, - "node_modules/from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "node_modules/from2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/from2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/from2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, - "node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -2660,16 +2373,6 @@ "node": ">=10" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-east-asian-width": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", @@ -2774,37 +2477,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2824,16 +2496,6 @@ "devOptional": true, "license": "ISC" }, - "node_modules/has": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", - "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -3066,23 +2728,6 @@ "node": ">=10.13.0" } }, - "node_modules/into-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", - "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "from2": "^2.3.0", - "p-is-promise": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -3258,13 +2903,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3291,19 +2929,6 @@ "license": "MIT", "optional": true }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -3324,19 +2949,6 @@ "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -3524,16 +3136,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -3543,33 +3145,6 @@ "node": ">= 0.6" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -3768,31 +3343,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/multistream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", - "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "once": "^1.4.0", - "readable-stream": "^3.6.0" - } - }, "node_modules/nan": { "version": "2.22.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", @@ -4227,16 +3777,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/p-is-promise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", - "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -4284,16 +3824,6 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4314,221 +3844,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.8.1.tgz", - "integrity": "sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/generator": "7.18.2", - "@babel/parser": "7.18.4", - "@babel/types": "7.19.0", - "chalk": "^4.1.2", - "fs-extra": "^9.1.0", - "globby": "^11.1.0", - "into-stream": "^6.0.0", - "is-core-module": "2.9.0", - "minimist": "^1.2.6", - "multistream": "^4.1.0", - "pkg-fetch": "3.4.2", - "prebuild-install": "7.1.1", - "resolve": "^1.22.0", - "stream-meter": "^1.0.4" - }, - "bin": { - "pkg": "lib-es5/bin.js" - }, - "peerDependencies": { - "node-notifier": ">=9.0.1" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/pkg-fetch": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.4.2.tgz", - "integrity": "sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "fs-extra": "^9.1.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.6", - "progress": "^2.0.3", - "semver": "^7.3.5", - "tar-fs": "^2.1.1", - "yargs": "^16.2.0" - }, - "bin": { - "pkg-fetch": "lib-es5/bin.js" - } - }, - "node_modules/pkg-fetch/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/pkg-fetch/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC" - }, - "node_modules/pkg-fetch/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/pkg-fetch/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/pkg-fetch/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/pkg-fetch/node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dev": true, - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/pkg/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/pkg/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/pkg/node_modules/is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/pkg/node_modules/prebuild-install": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", - "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pkg/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/playwright": { "version": "1.49.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", @@ -4587,23 +3902,6 @@ "node": ">=10" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -4693,27 +3991,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4809,16 +4086,6 @@ "regexp-tree": "bin/regexp-tree" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -4897,17 +4164,6 @@ "node": ">= 4" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4924,30 +4180,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5195,16 +4427,6 @@ "dev": true, "license": "MIT" }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -5350,49 +4572,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stream-meter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", - "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.1.4" - } - }, - "node_modules/stream-meter/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/stream-meter/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/stream-meter/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -5642,16 +4821,6 @@ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5941,16 +5110,6 @@ "imurmurhash": "^0.1.4" } }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6099,40 +5258,12 @@ "node": ">= 12.0.0" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "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/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/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -6148,35 +5279,6 @@ "node": ">= 6" } }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 78ac9460..5e85eced 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "dependency-cruiser": "^16.5.0", "nodemon": "^3.1.7", "ora": "^8.1.1", - "pkg": "^5.8.1", "ts-node": "^10.9.2", "tsx": "^4.19.2", "uglify-js": "^3.19.3" diff --git a/src/init.ts b/src/init.ts index 3979eb6f..6d3854a0 100644 --- a/src/init.ts +++ b/src/init.ts @@ -1,4 +1,5 @@ import express, { Request, Response, NextFunction } from "express"; +import process from "node:process"; import swaggerDocs from "./utils/swaggerDocs"; import auth from "./routes/auth/routes"; import data from "./routes/data/routes"; @@ -13,6 +14,7 @@ import { limiter } from "./middleware/rateLimiter"; import { scheduleFetch } from "./controllers/scheduler"; import cors from "cors"; import { blockWhileLocked } from "./middleware/checkLock"; +import logger from "./utils/logger"; const initializeApp = (app: express.Application): void => { app.use(cors()); @@ -42,6 +44,33 @@ const initializeApp = (app: express.Application): void => { app.get("/", (req: Request, res: Response) => { res.redirect("/api-docs"); }); + + process.on("exit", (code: number) => { + logger.warn(`Server exiting (Code: ${code})`); + console.log(` + \u001b[1;31mThank you for using\u001b[0m + + \u001b[1;34m###### ###### #### ### ### #### ######### ###### #########\u001b[0m + \u001b[1;34m### ### ### ### ### ### ### ### ### ### ### ###\u001b[0m + \u001b[1;34m### ### ### ### ### ###### #### ### ### ### ###\u001b[0m + \u001b[1;34m### ### ### ### ### ### ### #### ### ############ ###\u001b[0m + \u001b[1;34m### ### ### ### ### ### ### #### ### ### ### ###\u001b[0m + \u001b[1;34m###### ###### #### ### ### #### ### ### ### ### \u001b[0m(\u001b[1;33mAPI - v2.0.0\u001b[0m) + + \u001b[1;36mUseful links before you go:\u001b[0m + + - Documentation: \u001b[1;32mhttps://outline.itsnik.de/s/dockstat\u001b[0m + - GitHub (Frontend): \u001b[1;32mhttps://github.com/its4nik/dockstat\u001b[0m + - GitHub (Backend): \u001b[1;32mhttps://github.com/its4nik/dockstatapi\u001b[0m + - API Documentation: \u001b[1;32mhttp://localhost:7000/api-docs\u001b[0m + + \u001b[1;35mSummary:\u001b[0m + + DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. + + \u001b[1;31mGoodbye! We hope to see you again soon.\u001b[0m + `); + }); }; export default initializeApp; diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt index 7e77f2c9..995f295d 100644 --- a/src/misc/dependencyGraphs/mermaid-all.txt +++ b/src/misc/dependencyGraphs/mermaid-all.txt @@ -1,106 +1,125 @@ flowchart LR 0["server.ts"] -subgraph 1["controllers"] -2["highAvailability.ts"] -3["scheduler.ts"] -6["fetchData.ts"] -K["frontendConfiguration.ts"] +subgraph 1["config"] +2["hostsystem.ts"] +B["db.ts"] +1G["swaggerConfig.ts"] end -subgraph 4["config"] -5["db.ts"] -19["swaggerConfig.ts"] +3["os"] +subgraph 4["controllers"] +5["highAvailability.ts"] +9["proxy.ts"] +A["scheduler.ts"] +C["fetchData.ts"] +R["frontendConfiguration.ts"] end -subgraph 7["utils"] -8["containerService.ts"] -9["dockerClient.ts"] -N["connectionChecker.ts"] -P["extractHostData.ts"] -Q["writeOfflineLog.ts"] -subgraph V["notifications"] -W["_notify.ts"] -X["discord.ts"] -Y["_template.ts"] -Z["email.ts"] -10["pushbullet.ts"] -11["pushover.ts"] -12["slack.ts"] -13["telegram.ts"] -14["whatsapp.ts"] +6["util"] +7["init.ts"] +8["process"] +subgraph D["utils"] +E["containerService.ts"] +F["dockerClient.ts"] +U["connectionChecker.ts"] +W["extractHostData.ts"] +X["writeOfflineLog.ts"] +subgraph 12["notifications"] +13["_notify.ts"] +14["discord.ts"] +16["_template.ts"] +17["email.ts"] +18["pushbullet.ts"] +19["pushover.ts"] +1A["slack.ts"] +1B["telegram.ts"] +1C["whatsapp.ts"] end +1F["swaggerDocs.ts"] end -subgraph A["middleware"] -B["authMiddleware.ts"] -C["rateLimiter.ts"] +subgraph G["middleware"] +H["authMiddleware.ts"] +I["checkLock.ts"] +J["rateLimiter.ts"] end -subgraph D["routes"] -subgraph E["auth"] -F["routes.ts"] -end -subgraph G["data"] -H["routes.ts"] +subgraph K["routes"] +subgraph L["auth"] +M["routes.ts"] end -subgraph I["frontendController"] -J["routes.ts"] +subgraph N["data"] +O["routes.ts"] end -subgraph L["getter"] -M["routes.ts"] +subgraph P["frontendController"] +Q["routes.ts"] end -subgraph R["highavailability"] -S["routes.ts"] +subgraph S["getter"] +T["routes.ts"] end -subgraph T["notifications"] -U["routes.ts"] +subgraph Y["highavailability"] +Z["routes.ts"] end -subgraph 15["setter"] -16["routes.ts"] +subgraph 10["notifications"] +11["routes.ts"] end +subgraph 1D["setter"] +1E["routes.ts"] end -O["net"] -subgraph 17["swagger"] -18["swaggerDocs.ts"] end +V["net"] +15["https"] 0-->2 -0-->3 -0-->B -0-->C -0-->F -0-->H -0-->J -0-->M -0-->S -0-->U -0-->16 -0-->18 -3-->5 -3-->6 -6-->5 -6-->8 -8-->9 -H-->5 -J-->K -M-->3 -M-->N -M-->8 -M-->9 -M-->P -M-->Q -N-->O -S-->2 -U-->W -W-->X -W-->Z -W-->10 -W-->11 -W-->12 -W-->13 -W-->14 -X-->Y -Z-->Y -10-->Y -11-->Y -12-->Y -13-->Y -14-->Y -16-->3 -18-->19 +0-->5 +0-->7 +2-->3 +5-->6 +7-->9 +7-->A +7-->H +7-->I +7-->J +7-->M +7-->O +7-->Q +7-->T +7-->Z +7-->11 +7-->1E +7-->1F +7-->8 +A-->B +A-->C +C-->B +C-->E +E-->F +O-->B +Q-->R +T-->A +T-->U +T-->E +T-->F +T-->W +T-->X +U-->V +Z-->5 +11-->13 +13-->14 +13-->17 +13-->18 +13-->19 +13-->1A +13-->1B +13-->1C +14-->16 +14-->15 +17-->16 +18-->16 +18-->15 +19-->16 +19-->15 +1A-->16 +1A-->15 +1B-->16 +1B-->15 +1C-->16 +1C-->15 +1E-->A +1F-->1G diff --git a/src/utils/removeUnusedDeps.sh b/src/utils/removeUnusedDeps.sh index b5b68ebf..df72f4b4 100755 --- a/src/utils/removeUnusedDeps.sh +++ b/src/utils/removeUnusedDeps.sh @@ -2,7 +2,7 @@ echo "Creating unused dependency list" -TMP="$(npx depcheck --ignores @types/supports-color,ipaddr.js,dependency-cruiser,tsx,@types/bcrypt,@types/express,@types/express-handlebars,@types/node,ts-node --quiet --oneline | tail -n 1 | tr -d '\n')" +TMP="$(npx depcheck --ignores @types/node-fetch,uglify-js,@types/supports-color,ipaddr.js,dependency-cruiser,tsx,@types/bcrypt,@types/express,@types/express-handlebars,@types/node,ts-node --quiet --oneline | tail -n 1 | tr -d '\n')" lines=$(echo "$TMP" | tr -s ' ' '\n' | wc -l) diff --git a/yarn.lock b/yarn.lock index 1418f00e..9c800499 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,39 +34,6 @@ call-me-maybe "^1.0.1" z-schema "^5.0.1" -"@babel/generator@7.18.2": - version "7.18.2" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz" - integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw== - dependencies: - "@babel/types" "^7.18.2" - "@jridgewell/gen-mapping" "^0.3.0" - jsesc "^2.5.1" - -"@babel/helper-string-parser@^7.18.10": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz" - integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== - -"@babel/helper-validator-identifier@^7.18.6": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz" - integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== - -"@babel/parser@7.18.4": - version "7.18.4" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz" - integrity sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow== - -"@babel/types@^7.18.2", "@babel/types@7.19.0": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz" - integrity sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA== - dependencies: - "@babel/helper-string-parser" "^7.18.10" - "@babel/helper-validator-identifier" "^7.18.6" - to-fast-properties "^2.0.0" - "@balena/dockerignore@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz" @@ -103,38 +70,16 @@ resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== -"@jridgewell/gen-mapping@^0.3.0": - version "0.3.5" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== - dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.0.3": version "3.1.2" resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": +"@jridgewell/sourcemap-codec@^1.4.10": version "1.5.0" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@^0.3.24": - version "0.3.25" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" @@ -163,27 +108,6 @@ semver "^7.3.5" tar "^6.1.11" -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": - version "2.0.5" - resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - "@npmcli/fs@^1.0.0": version "1.1.1" resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz" @@ -493,7 +417,7 @@ ansi-regex@^6.0.1: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz" integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -544,11 +468,6 @@ array-flatten@1.1.1: resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - asn1@^0.2.6: version "0.2.6" resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" @@ -566,11 +485,6 @@ asynckit@^0.4.0: resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" @@ -643,7 +557,7 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.3, braces@~3.0.2: +braces@~3.0.2: version "3.0.3" resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -723,14 +637,6 @@ chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - chalk@^5.3.0: version "5.3.0" resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" @@ -785,15 +691,6 @@ cli-spinners@^2.9.2: resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz" integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - color-convert@^1.9.3: version "1.9.3" resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" @@ -901,11 +798,6 @@ cookie@0.7.1: resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz" integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - cors@^2.8.5: version "2.8.5" resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" @@ -1025,13 +917,6 @@ diff@^4.0.1: resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - docker-modem@^5.0.3: version "5.0.3" resolved "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz" @@ -1169,11 +1054,6 @@ esbuild@~0.23.0: "@esbuild/win32-ia32" "0.23.1" "@esbuild/win32-x64" "0.23.1" -escalade@^3.1.1: - version "3.2.0" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" @@ -1241,29 +1121,11 @@ fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.9: - version "3.3.2" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - fast-uri@^3.0.1: version "3.0.3" resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz" integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== -fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== - dependencies: - reusify "^1.0.4" - fecha@^4.2.0: version "4.2.3" resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz" @@ -1333,29 +1195,11 @@ fresh@0.5.2: resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== -from2@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz" - integrity sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g== - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.0" - fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@^9.1.0: - version "9.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" @@ -1402,11 +1246,6 @@ gauge@^4.0.3: strip-ansi "^6.0.1" wide-align "^1.1.5" -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - get-east-asian-width@^1.0.0: version "1.3.0" resolved "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz" @@ -1438,7 +1277,7 @@ github-from-package@0.0.0: resolved "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz" integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== -glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -1476,24 +1315,12 @@ global-directory@^4.0.1: dependencies: ini "4.1.1" -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6: +graceful-fs@^4.2.4, graceful-fs@^4.2.6: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -1525,11 +1352,6 @@ has-unicode@^2.0.1: resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== -has@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/has/-/has-1.0.4.tgz" - integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== - hasown@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" @@ -1606,11 +1428,6 @@ ignore-by-default@^1.0.1: resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz" integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== -ignore@^5.2.0: - version "5.3.2" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - ignore@^6.0.2: version "6.0.2" resolved "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz" @@ -1639,7 +1456,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@2, inherits@2.0.4: +inherits@^2.0.3, inherits@^2.0.4, inherits@2, inherits@2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1659,14 +1476,6 @@ interpret@^3.1.1: resolved "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz" integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== -into-stream@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz" - integrity sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA== - dependencies: - from2 "^2.3.0" - p-is-promise "^3.0.0" - ip-address@^9.0.5: version "9.0.5" resolved "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz" @@ -1704,13 +1513,6 @@ is-core-module@^2.13.0: dependencies: hasown "^2.0.2" -is-core-module@2.9.0: - version "2.9.0" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz" - integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== - dependencies: - has "^1.0.3" - is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" @@ -1771,11 +1573,6 @@ is-unicode-supported@^2.0.0: resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz" integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ== -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -1793,11 +1590,6 @@ jsbn@1.1.0: resolved "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz" integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - json-schema-traverse@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" @@ -1808,15 +1600,6 @@ json5@^2.2.2, json5@^2.2.3: resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - kleur@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" @@ -1920,24 +1703,11 @@ merge-descriptors@1.0.3: resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - methods@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^4.0.4: - version "4.0.8" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - mime-db@1.52.0: version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" @@ -2056,14 +1826,6 @@ ms@2.0.0: resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -multistream@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz" - integrity sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw== - dependencies: - once "^1.4.0" - readable-stream "^3.6.0" - nan@^2.19.0, nan@^2.20.0: version "2.22.0" resolved "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz" @@ -2101,13 +1863,6 @@ node-domexception@^1.0.0: resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== -node-fetch@^2.6.6: - version "2.7.0" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" @@ -2251,11 +2006,6 @@ ora@^8.1.1: string-width "^7.2.0" strip-ansi "^7.1.0" -p-is-promise@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz" - integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ== - p-map@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" @@ -2283,11 +2033,6 @@ path-to-regexp@0.1.12: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz" integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - picocolors@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" @@ -2303,50 +2048,11 @@ picomatch@^2.2.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - picomatch@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz" integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== -pkg-fetch@3.4.2: - version "3.4.2" - resolved "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.4.2.tgz" - integrity sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA== - dependencies: - chalk "^4.1.2" - fs-extra "^9.1.0" - https-proxy-agent "^5.0.0" - node-fetch "^2.6.6" - progress "^2.0.3" - semver "^7.3.5" - tar-fs "^2.1.1" - yargs "^16.2.0" - -pkg@^5.8.1: - version "5.8.1" - resolved "https://registry.npmjs.org/pkg/-/pkg-5.8.1.tgz" - integrity sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA== - dependencies: - "@babel/generator" "7.18.2" - "@babel/parser" "7.18.4" - "@babel/types" "7.19.0" - chalk "^4.1.2" - fs-extra "^9.1.0" - globby "^11.1.0" - into-stream "^6.0.0" - is-core-module "2.9.0" - minimist "^1.2.6" - multistream "^4.1.0" - pkg-fetch "3.4.2" - prebuild-install "7.1.1" - resolve "^1.22.0" - stream-meter "^1.0.4" - playwright-core@1.49.0: version "1.49.0" resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz" @@ -2379,34 +2085,6 @@ prebuild-install@^7.1.1: tar-fs "^2.0.0" tunnel-agent "^0.6.0" -prebuild-install@7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz" - integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== - dependencies: - detect-libc "^2.0.0" - expand-template "^2.0.3" - github-from-package "0.0.0" - minimist "^1.2.3" - mkdirp-classic "^0.5.3" - napi-build-utils "^1.0.1" - node-abi "^3.3.0" - pump "^3.0.0" - rc "^1.2.7" - simple-get "^4.0.0" - tar-fs "^2.0.0" - tunnel-agent "^0.6.0" - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -progress@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz" @@ -2456,11 +2134,6 @@ qs@6.13.0: dependencies: side-channel "^1.0.6" -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - range-parser@~1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" @@ -2486,32 +2159,6 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -readable-stream@^2.0.0: - version "2.3.8" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^2.1.4: - version "2.3.8" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" @@ -2545,11 +2192,6 @@ regexp-tree@~0.1.1: resolved "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz" integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - require-from-string@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" @@ -2560,7 +2202,7 @@ resolve-pkg-maps@^1.0.0: resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== -resolve@^1.20.0, resolve@^1.22.0: +resolve@^1.20.0: version "1.22.8" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -2582,11 +2224,6 @@ retry@^0.12.0: resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - rimraf@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" @@ -2594,23 +2231,11 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - safe-buffer@^5.0.1, safe-buffer@~5.2.0, safe-buffer@5.2.1: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-regex@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz" @@ -2742,11 +2367,6 @@ sisteransi@^1.0.5: resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" @@ -2824,13 +2444,6 @@ stdin-discarder@^0.2.2: resolved "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz" integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ== -stream-meter@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz" - integrity sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ== - dependencies: - readable-stream "^2.1.4" - string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" @@ -2838,14 +2451,7 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -2863,7 +2469,7 @@ string-width@^7.2.0: get-east-asian-width "^1.0.0" strip-ansi "^7.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -2954,17 +2560,7 @@ tar-fs@^2.0.0, tar-fs@~2.0.1: pump "^3.0.0" tar-stream "^2.0.0" -tar-fs@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.1.4" - -tar-stream@^2.0.0, tar-stream@^2.1.4: +tar-stream@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz" integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== @@ -2997,11 +2593,6 @@ text-hex@1.0.x: resolved "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" @@ -3136,17 +2727,12 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" -universalify@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" - integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== - unpipe@~1.0.0, unpipe@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== @@ -3234,25 +2820,11 @@ winston@^3.15.0: triple-beam "^1.3.0" winston-transport "^4.9.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrappy@1: version "1.0.2" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - yallist@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" @@ -3263,24 +2835,6 @@ yaml@2.0.0-1: resolved "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz" integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - yn@3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" From aeaef0a4c1c351a1bac751087f272e78e4870958 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:44:56 +0100 Subject: [PATCH 021/369] Fix: remove verbose flag in workflow Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 87792b05..df4f276c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ RUN apk update && \ COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ -RUN npm install --verbose +RUN npm install COPY ./src ./src RUN npm run build:mini From 0a9f32600694e7843ea7733db0e98a215b6b9dda Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:46:57 +0100 Subject: [PATCH 022/369] Fix: Fixing typo in ReadMe Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae34767f..e6a17bb1 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ With this new release a couple of extra features (compared to v1) are going to b - Multi-arch docker builds (using buildx github action) - Advanced security through middlewares: rate-limiting and authentication - Multi Arch Docker builds through docker buildx -- High Availability using single master and ulimited worker nodes! +- High Availability using single master and unlimited worker nodes! # 🔗 DockStatAPI v2 Documentation From 4af32e873635f9a9d0922544cb678653fd61fde3 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:48:32 +0100 Subject: [PATCH 023/369] Fix: CodeQL Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/controllers/frontendConfiguration.ts | 32 ++++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/controllers/frontendConfiguration.ts b/src/controllers/frontendConfiguration.ts index 6a5a6911..e83eaaee 100644 --- a/src/controllers/frontendConfiguration.ts +++ b/src/controllers/frontendConfiguration.ts @@ -185,22 +185,22 @@ async function setIcon(containerName: string, icon: string, custom: boolean) { ); if (custom === true) { - if (containerIndex !== -1) { - data[containerIndex].icon = `custom/${icon}`; - await saveData(data); - } else { - data.push({ name: containerName, icon: `custom/${icon}` }); - await saveData(data); - } - } else { - if (containerIndex !== -1) { - data[containerIndex].icon = `${icon}`; - await saveData(data); - } else { - data.push({ name: containerName, icon: `${icon}` }); - await saveData(data); - } - } + if (containerIndex !== -1) { + data[containerIndex].icon = `custom/${icon}`; + await saveData(data); + } else { + data.push({ name: containerName, icon: `custom/${icon}` }); + await saveData(data); + } + } + else if (containerIndex !== -1) { + data[containerIndex].icon = `${icon}`; + await saveData(data); + } + else { + data.push({ name: containerName, icon: `${icon}` }); + await saveData(data); + } } catch (error: any) { logger.error(error); throw new Error(error); From 00e9ffe396bc85f3709575982a56ff0635f9d025 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:49:15 +0100 Subject: [PATCH 024/369] Fix: Use object destruction Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/routes/getter/routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/getter/routes.ts b/src/routes/getter/routes.ts index 0f9883f9..c559e637 100644 --- a/src/routes/getter/routes.ts +++ b/src/routes/getter/routes.ts @@ -134,7 +134,7 @@ router.get("/system", (req: Request, res: Response) => { * description: Error message detailing the issue encountered. */ router.get("/host/:hostName/stats", async (req: Request, res: Response) => { - const hostName = req.params.hostName; + const {hostName} = req.params; logger.info(`Fetching stats for host: ${hostName}`); if (process.env.OFFLINE === "true") { logger.info("Fetching offline Host Stats"); From 47ff28c6692eba5e5f62ffa57c2a5e96c454a285 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:49:55 +0100 Subject: [PATCH 025/369] Fix: CodeQL (braces for if clauses) Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/utils/notifications/_template.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/notifications/_template.ts b/src/utils/notifications/_template.ts index ecc327e1..2b6e3a42 100644 --- a/src/utils/notifications/_template.ts +++ b/src/utils/notifications/_template.ts @@ -44,7 +44,7 @@ function renderTemplate(containerId: string) { let containerData = null; for (const host in containers) { containerData = containers[host].find((c: any) => c.id === containerId); - if (containerData) break; + if (containerData) { } if (!containerData) { From 5404206ea5e649a6ac4ebeed828a5d7ce2783a94 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:52:12 +0100 Subject: [PATCH 026/369] Chore: Update Dockerfile --- Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index df4f276c..7bce2028 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ LABEL description="API for DockStat" LABEL license="BSD-3-Clause license" LABEL repository="https://github.com/its4nik/dockstatapi" LABEL documentation="https://github.com/its4nik/dockstatapi" -LABEL org.opencontainers.image.description "The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" +LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" LABEL org.opencontainers.image.licenses="BSD-3-Clause license" LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" @@ -48,8 +48,6 @@ RUN node src/config/db.js # Stage 3: Production stage FROM alpine AS production -ARG RUNNING_IN_DOCKER=true -RUN apk add --update bash nodejs WORKDIR /api From 7e2ca2049c43bea91a9b3a944d556e11298d156b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:55:10 +0100 Subject: [PATCH 027/369] Chore: Update _template.ts --- src/utils/notifications/_template.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/notifications/_template.ts b/src/utils/notifications/_template.ts index 2b6e3a42..88439945 100644 --- a/src/utils/notifications/_template.ts +++ b/src/utils/notifications/_template.ts @@ -45,6 +45,7 @@ function renderTemplate(containerId: string) { for (const host in containers) { containerData = containers[host].find((c: any) => c.id === containerId); if (containerData) { + break; } if (!containerData) { From 71b8e2fa1a66a59e75561ab21dbad8bbd5187c08 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 21 Dec 2024 13:56:35 +0100 Subject: [PATCH 028/369] Update _template.ts --- src/utils/notifications/_template.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/notifications/_template.ts b/src/utils/notifications/_template.ts index 88439945..bbec96f4 100644 --- a/src/utils/notifications/_template.ts +++ b/src/utils/notifications/_template.ts @@ -45,7 +45,7 @@ function renderTemplate(containerId: string) { for (const host in containers) { containerData = containers[host].find((c: any) => c.id === containerId); if (containerData) { - break; + break(); } if (!containerData) { From b0c266cf6df94413f1f71a6f052c56f55547e19b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 08:30:49 +0100 Subject: [PATCH 029/369] Chore: Add npm run docker(:build) commands --- .gitignore | 1 + docker-compose.yaml | 23 +++++++++++++++++++++++ package.json | 14 ++++++++------ src/data/usePassword.txt | 2 +- 4 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 docker-compose.yaml diff --git a/.gitignore b/.gitignore index 43ddf882..fbbbb21b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ src/data/user.conf src/data/password.json src/data/ha.lock +docker .test* # Created by https://www.toptal.com/developers/gitignore/api/node ### Node ### diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..721b8cc7 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,23 @@ +services: + master: + container_name: master + environment: + - NODE_ENV=development + - HA_MASTER=true + - HA_MASTER_IP=localhost:9876 + - HA_NODE=localhost:6789 + - HA_UNSAFE=true + volumes: + - ./docker/master:/api/src/data + ports: + - 9876:9876 + image: dockstatapi:local + slave: + container_name: slave + environment: + - NODE_ENV=development + volumes: + - ./docker/slave:/api/src/data + ports: + - 6789:6789 + image: dockstatapi:local diff --git a/package.json b/package.json index 5e85eced..9517b02f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dockstatapi", - "version": "2", + "version": "2.0.0", "description": "API for docker hosts using dockerode", "main": "src/server.ts", "scripts": { @@ -12,16 +12,14 @@ "dep:remove": "bash ./src/utils/removeUnusedDeps.sh && bash ./src/utils/createDependencyGraph.sh", "build": "npx tsc", "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", - "mini": "bash ./src/misc/minifyDist.sh" + "mini": "bash ./src/misc/minifyDist.sh", + "docker": "sudo docker compose up", + "docker:build": "sudo docker build . -t \"dockstatapi:local\" && sudo docker compose up" }, "keywords": [], "author": "Its4Nik", "license": "BSD 3-Clause License", "dependencies": { - "@types/dockerode": "^3.3.31", - "@types/supports-color": "^8.1.3", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.7", "bcrypt": "^5.1.1", "chokidar": "^4.0.1", "cors": "^2.8.5", @@ -38,6 +36,10 @@ "winston": "^3.15.0" }, "devDependencies": { + "@types/dockerode": "^3.3.31", + "@types/supports-color": "^8.1.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.7", "@playwright/test": "^1.49.0", "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", diff --git a/src/data/usePassword.txt b/src/data/usePassword.txt index 02e4a84d..c508d536 100644 --- a/src/data/usePassword.txt +++ b/src/data/usePassword.txt @@ -1 +1 @@ -false \ No newline at end of file +false From 72d4c12e884e3e1c2fc73cb6308c1be00b729379 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 08:35:06 +0100 Subject: [PATCH 030/369] Fix: Fixing 'break' syntax, in _template.ts --- Dockerfile | 2 +- src/utils/notifications/_template.ts | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7bce2028..59a59ca7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,7 @@ WORKDIR /build RUN mkdir -p /build/src/data COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ -RUN npm install --omit=dev --verbose +RUN npm install --omit=dev COPY --from=builder /build/dist/* /build/src COPY --from=builder /build/src/misc/entrypoint.sh /build/entrypoint.sh diff --git a/src/utils/notifications/_template.ts b/src/utils/notifications/_template.ts index bbec96f4..551da826 100644 --- a/src/utils/notifications/_template.ts +++ b/src/utils/notifications/_template.ts @@ -1,13 +1,14 @@ import fs from "fs"; import logger from "../logger"; + const templatePath: string = "./src/data/template.json"; const containersPath: string = "./src/data/states.json"; interface Template { - "text": string + text: string; } -function getTemplate() { +function getTemplate(): Template | null { try { const data = fs.readFileSync(templatePath, "utf8"); return JSON.parse(data); @@ -17,11 +18,11 @@ function getTemplate() { } } -function setTemplate(newTemplate: string) { +function setTemplate(newTemplate: string): void { try { fs.writeFileSync( templatePath, - JSON.stringify(newTemplate, null, 2), + JSON.stringify({ text: newTemplate }, null, 2), "utf8", ); logger.debug("Template updated successfully"); @@ -30,8 +31,8 @@ function setTemplate(newTemplate: string) { } } -function renderTemplate(containerId: string) { - const template: Template = getTemplate(); +function renderTemplate(containerId: string): string | null { + const template = getTemplate(); if (!template) { logger.error("Template is missing or not a string"); return null; @@ -41,11 +42,12 @@ function renderTemplate(containerId: string) { const data = fs.readFileSync(containersPath, "utf8"); const containers = JSON.parse(data); - let containerData = null; + let containerData: Record | null = null; for (const host in containers) { containerData = containers[host].find((c: any) => c.id === containerId); if (containerData) { - break(); + break; + } } if (!containerData) { @@ -65,5 +67,4 @@ function renderTemplate(containerId: string) { } } - export { getTemplate, setTemplate, renderTemplate }; From 84cfbf39ee4847d5cce931d15930354d63849da2 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 08:45:58 +0100 Subject: [PATCH 031/369] Feat: Add CI/CD Badges to ReadMe --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e6a17bb1..6fd1f001 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # DockStatAPI v2 ![Dockstat Logo](.github/DockStat.png) +*Pipelines:* +Main: [![GitHubCI pipeline status badge](https://github.com/its4nik/dokstatapi/workflows/build-main/badge.svg?branch=main)](https://github.com/its4nik/dokstatapi/commits/main) +Dev : [![GitHubCI pipeline status badge](https://github.com/its4nik/dokstatapi/workflows/build-dev/badge.svg?branch=dev)](https://github.com/its4nik/dokstatapi/commits/dev) + This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. With this new release a couple of extra features (compared to v1) are going to be available. From 1e16f950e452ca0bd6135b2b19abe75ca03e678c Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 08:47:50 +0100 Subject: [PATCH 032/369] Fix: Change badges --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6fd1f001..034c0095 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ ![Dockstat Logo](.github/DockStat.png) *Pipelines:* -Main: [![GitHubCI pipeline status badge](https://github.com/its4nik/dokstatapi/workflows/build-main/badge.svg?branch=main)](https://github.com/its4nik/dokstatapi/commits/main) -Dev : [![GitHubCI pipeline status badge](https://github.com/its4nik/dokstatapi/workflows/build-dev/badge.svg?branch=dev)](https://github.com/its4nik/dokstatapi/commits/dev) +[![Docker Image CI](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml/badge.svg?branch=main)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml) +[![Build dockstatapi:nightly](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml) This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. With this new release a couple of extra features (compared to v1) are going to be available. From f2322bc907a4495209b8ef881f38019261eaed6b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 08:48:20 +0100 Subject: [PATCH 033/369] Fix: Update README.md with newlines between the badges --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 034c0095..f602f3b2 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # DockStatAPI v2 ![Dockstat Logo](.github/DockStat.png) -*Pipelines:* -[![Docker Image CI](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml/badge.svg?branch=main)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml) -[![Build dockstatapi:nightly](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml) +*Pipelines:*
+[![Docker Image CI](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml/badge.svg?branch=main)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml)
+[![Build dockstatapi:nightly](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml)
This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. With this new release a couple of extra features (compared to v1) are going to be available. From 1f0c5d652199121745ddba92ad825336bef50375 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 13:23:29 +0100 Subject: [PATCH 034/369] Fix: Needs bash and curl for healthcheck / entrypoint --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 59a59ca7..5e49bbd0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,6 +49,10 @@ RUN node src/config/db.js # Stage 3: Production stage FROM alpine AS production +RUN apk add --update bash curl +HEALTHCHECK --interval=5m --timeout=3s \ + curl -f http://localhost:9876/api/status || exit 1 + WORKDIR /api COPY --from=main /build /api From 0de6ba9c6fe53c5669b3553a0e8a539cb75d1040 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 13:24:45 +0100 Subject: [PATCH 035/369] Fix: Add 'CMD' to HEALTHCHECK --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5e49bbd0..f463522d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,7 +51,7 @@ FROM alpine AS production RUN apk add --update bash curl HEALTHCHECK --interval=5m --timeout=3s \ - curl -f http://localhost:9876/api/status || exit 1 + CMD curl -f http://localhost:9876/api/status || exit 1 WORKDIR /api From 522b2d093ba86a88d286cbd91376fa7d1d1d8c34 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 23 Dec 2024 23:27:00 +0100 Subject: [PATCH 036/369] Fix: Adding bash/nodejs binaries to last docker stage --- .gitignore | 1 + Dockerfile | 6 ++--- docker-compose.yaml | 42 +++++++++++++++++++++++++++-- package-lock.json | 32 +++++++++++++++++----- package.json | 6 +++-- src/config/loggerConfig.ts | 11 ++++++++ src/data/frontendConfiguration.json | 8 ++++++ src/data/usePassword.txt | 2 +- src/init.ts | 23 ---------------- src/server.ts | 6 +++-- 10 files changed, 98 insertions(+), 39 deletions(-) create mode 100644 src/data/frontendConfiguration.json diff --git a/.gitignore b/.gitignore index fbbbb21b..c7f5c64e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ src/data/states.json src/data/user.conf src/data/password.json src/data/ha.lock +src/data/frontendConfiguration.json docker .test* diff --git a/Dockerfile b/Dockerfile index f463522d..78ee53b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM node:alpine AS builder LABEL maintainer="https://github.com/its4nik" -LABEL version="2" +LABEL version="2.0.0" LABEL description="API for DockStat" LABEL license="BSD-3-Clause license" LABEL repository="https://github.com/its4nik/dockstatapi" @@ -49,9 +49,9 @@ RUN node src/config/db.js # Stage 3: Production stage FROM alpine AS production -RUN apk add --update bash curl +RUN apk add --update bash curl nodejs HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:9876/api/status || exit 1 + CMD curl -f http://localhost:9876/api/status || exit 1 WORKDIR /api diff --git a/docker-compose.yaml b/docker-compose.yaml index 721b8cc7..31586575 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,8 +4,8 @@ services: environment: - NODE_ENV=development - HA_MASTER=true - - HA_MASTER_IP=localhost:9876 - - HA_NODE=localhost:6789 + - HA_MASTER_IP=127.0.0.1:9876 + - HA_NODE=127.0.0.1:6789 - HA_UNSAFE=true volumes: - ./docker/master:/api/src/data @@ -21,3 +21,41 @@ services: ports: - 6789:6789 image: dockstatapi:local + + test-socket-proxy: + image: lscr.io/linuxserver/socket-proxy:latest + container_name: socket-proxy + environment: + - ALLOW_START=1 #optional + - ALLOW_STOP=1 #optional + - ALLOW_RESTARTS=1 #optional + - AUTH=0 #optional + - BUILD=0 #optional + - COMMIT=0 #optional + - CONFIGS=0 #optional + - CONTAINERS=1 #optional + - DISABLE_IPV6=0 #optional + - DISTRIBUTION=0 #optional + - EVENTS=1 #optional + - EXEC=0 #optional + - IMAGES=0 #optional + - INFO=1 #optional + - NETWORKS=1 #optional + - NODES=1 #optional + - PING=1 #optional + - POST=0 #optional + - PLUGINS=0 #optional + - SECRETS=0 #optional + - SERVICES=0 #optional + - SESSION=0 #optional + - SWARM=0 #optional + - SYSTEM=0 #optional + - TASKS=0 #optional + - VERSION=1 #optional + - VOLUMES=0 #optional + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + restart: unless-stopped + read_only: true + tmpfs: + - /run diff --git a/package-lock.json b/package-lock.json index dcd2ac0a..27899c7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,14 @@ { "name": "dockstatapi", - "version": "2", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dockstatapi", - "version": "2", + "version": "2.0.0", "license": "BSD 3-Clause License", "dependencies": { - "@types/dockerode": "^3.3.31", - "@types/supports-color": "^8.1.3", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.7", "bcrypt": "^5.1.1", "chokidar": "^4.0.1", "cors": "^2.8.5", @@ -32,11 +28,15 @@ "@playwright/test": "^1.49.0", "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", + "@types/dockerode": "^3.3.31", "@types/express": "^5.0.0", "@types/express-handlebars": "^5.3.1", "@types/node": "^22.9.0", "@types/node-fetch": "^2.6.12", "@types/nodemailer": "^6.4.17", + "@types/supports-color": "^8.1.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.7", "dependency-cruiser": "^16.5.0", "nodemon": "^3.1.7", "ora": "^8.1.1", @@ -721,6 +721,7 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -731,6 +732,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -750,6 +752,7 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -760,6 +763,7 @@ "version": "3.3.32", "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.32.tgz", "integrity": "sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg==", + "dev": true, "license": "MIT", "dependencies": { "@types/docker-modem": "*", @@ -771,6 +775,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -790,6 +795,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -802,6 +808,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -814,12 +821,14 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "22.10.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -850,18 +859,21 @@ "version": "6.9.17", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -872,6 +884,7 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -883,6 +896,7 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz", "integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "^18.11.18" @@ -892,6 +906,7 @@ "version": "18.19.67", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -901,24 +916,28 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, "license": "MIT" }, "node_modules/@types/supports-color": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", "integrity": "sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==", + "dev": true, "license": "MIT" }, "node_modules/@types/swagger-jsdoc": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/swagger-ui-express": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz", "integrity": "sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g==", + "dev": true, "license": "MIT", "dependencies": { "@types/express": "*", @@ -5088,6 +5107,7 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, "license": "MIT" }, "node_modules/unique-filename": { diff --git a/package.json b/package.json index 9517b02f..65478aa2 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "build": "npx tsc", "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", "mini": "bash ./src/misc/minifyDist.sh", - "docker": "sudo docker compose up", - "docker:build": "sudo docker build . -t \"dockstatapi:local\" && sudo docker compose up" + "docker": "sudo docker compose up -d", + "docker:full": "docker compose up -d && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", + "docker:build": "docker build . -t \"dockstatapi:local\" && docker compose up -d", + "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down" }, "keywords": [], "author": "Its4Nik", diff --git a/src/config/loggerConfig.ts b/src/config/loggerConfig.ts index 7d34f035..5d1a33e4 100644 --- a/src/config/loggerConfig.ts +++ b/src/config/loggerConfig.ts @@ -8,6 +8,16 @@ const green = "\x1b[32m"; const yellow = "\x1b[33m"; const blue = "\x1b[34m"; +const ignoreExitListenerLogs = format((info) => { + if ( + typeof info.message === "string" && + info.message.includes("Exit listeners detected") + ) { + return false; // Silences annoying logs + } + return info; +}); + function colorLog(level: string, levelName: string) { switch (level) { case "info": @@ -26,6 +36,7 @@ function colorLog(level: string, levelName: string) { const logger = createLogger({ level: "debug", format: format.combine( + ignoreExitListenerLogs(), format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), format.printf((info) => { const level = info.level.toUpperCase().padEnd(5, " "); diff --git a/src/data/frontendConfiguration.json b/src/data/frontendConfiguration.json new file mode 100644 index 00000000..4697f960 --- /dev/null +++ b/src/data/frontendConfiguration.json @@ -0,0 +1,8 @@ +[ + { + "name": "test", + "tags": [ + "123" + ] + } +] \ No newline at end of file diff --git a/src/data/usePassword.txt b/src/data/usePassword.txt index c508d536..02e4a84d 100644 --- a/src/data/usePassword.txt +++ b/src/data/usePassword.txt @@ -1 +1 @@ -false +false \ No newline at end of file diff --git a/src/init.ts b/src/init.ts index 6d3854a0..feaa00d1 100644 --- a/src/init.ts +++ b/src/init.ts @@ -47,29 +47,6 @@ const initializeApp = (app: express.Application): void => { process.on("exit", (code: number) => { logger.warn(`Server exiting (Code: ${code})`); - console.log(` - \u001b[1;31mThank you for using\u001b[0m - - \u001b[1;34m###### ###### #### ### ### #### ######### ###### #########\u001b[0m - \u001b[1;34m### ### ### ### ### ### ### ### ### ### ### ###\u001b[0m - \u001b[1;34m### ### ### ### ### ###### #### ### ### ### ###\u001b[0m - \u001b[1;34m### ### ### ### ### ### ### #### ### ############ ###\u001b[0m - \u001b[1;34m### ### ### ### ### ### ### #### ### ### ### ###\u001b[0m - \u001b[1;34m###### ###### #### ### ### #### ### ### ### ### \u001b[0m(\u001b[1;33mAPI - v2.0.0\u001b[0m) - - \u001b[1;36mUseful links before you go:\u001b[0m - - - Documentation: \u001b[1;32mhttps://outline.itsnik.de/s/dockstat\u001b[0m - - GitHub (Frontend): \u001b[1;32mhttps://github.com/its4nik/dockstat\u001b[0m - - GitHub (Backend): \u001b[1;32mhttps://github.com/its4nik/dockstatapi\u001b[0m - - API Documentation: \u001b[1;32mhttp://localhost:7000/api-docs\u001b[0m - - \u001b[1;35mSummary:\u001b[0m - - DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. - - \u001b[1;31mGoodbye! We hope to see you again soon.\u001b[0m - `); }); }; diff --git a/src/server.ts b/src/server.ts index 4853204b..6b680291 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,11 +7,13 @@ import writeUserConf from "./config/hostsystem"; const app = express(); const PORT: number = 9876; +logger.info("Server starting up..."); +logger.info(`Server is running on http://localhost:${PORT}`); +logger.info(`Swagger docs available at http://localhost:${PORT}/api-docs\n`); + writeUserConf(); initializeApp(app); app.listen(PORT, () => { - logger.info(`Server is running on http://localhost:${PORT}`); - logger.info(`Swagger docs available at http://localhost:${PORT}/api-docs`); startMasterNode(); }); From 45e3fc1f2f167a38ec011a198f27d810630e3016 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 25 Dec 2024 20:16:46 +0100 Subject: [PATCH 037/369] Chore: adjust process env and patch to 2.0.1 --- Dockerfile | 2 +- nodemon.json | 11 +++++- package.json | 17 +++----- src/config/hostsystem.ts | 5 ++- src/config/variables.ts | 24 ++++++++++++ src/controllers/fetchData.ts | 26 ++++++------ src/controllers/highAvailability.ts | 26 +++++++----- src/controllers/notificationController.ts | 48 +++++++++++------------ src/controllers/proxy.ts | 3 +- src/data/template.json | 3 ++ src/init.ts | 22 +++++------ src/misc/createEnvDev.sh | 32 +++++++++++++++ src/misc/createEnvFile.sh | 13 +++--- src/misc/entrypoint.sh | 5 +-- src/routes/getter/routes.ts | 35 +++++++---------- src/utils/notifications/_notify.ts | 33 ---------------- src/utils/notifications/discord.ts | 19 ++++----- src/utils/notifications/email.ts | 14 +++++-- src/utils/notifications/pushbullet.ts | 4 +- src/utils/notifications/pushover.ts | 25 ++++++------ src/utils/notifications/slack.ts | 19 ++++----- src/utils/notifications/telegram.ts | 23 +++++------ src/utils/notifications/whatsapp.ts | 21 +++++----- tsconfig.json | 12 ++---- 24 files changed, 233 insertions(+), 209 deletions(-) create mode 100644 src/config/variables.ts create mode 100644 src/data/template.json create mode 100755 src/misc/createEnvDev.sh diff --git a/Dockerfile b/Dockerfile index 78ee53b2..53f3b729 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM node:alpine AS builder LABEL maintainer="https://github.com/its4nik" -LABEL version="2.0.0" +LABEL version="2.0.1" LABEL description="API for DockStat" LABEL license="BSD-3-Clause license" LABEL repository="https://github.com/its4nik/dockstatapi" diff --git a/nodemon.json b/nodemon.json index 30602eb0..9d946e97 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,6 +1,13 @@ { - "ignore": ["src/logs", "**/fixtures/**", ".gitignore", "**/*.json"], + "ignore": [ + "**/data/**", + "src/logs", + "**/fixtures/**", + ".gitignore", + "**/*.json" + ], "execMap": { "ts": "tsx" - } + }, + "delay": 2500 } diff --git a/package.json b/package.json index 65478aa2..d600a792 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "dockstatapi", - "version": "2.0.0", + "version": "2.0.1", "description": "API for docker hosts using dockerode", "main": "src/server.ts", "scripts": { - "start": "tsx src/server.ts", + "local-env-file": "bash ./src/misc/createEnvDev.sh", + "start": "npm run local-env-file && tsx src/server.ts", "start:build": "npx tsc && node dist/server.js", - "dev": "nodemon", - "dev:trace": "nodemon --trace-uncaught --trace-warnings", + "dev": "npm run local-env-file && nodemon", + "dev:trace": "npm run local-env-file && nodemon --trace-uncaught --trace-warnings", "dep": "bash ./src/utils/createDependencyGraph.sh", "dep:remove": "bash ./src/utils/removeUnusedDeps.sh && bash ./src/utils/createDependencyGraph.sh", "build": "npx tsc", @@ -57,14 +58,6 @@ "tsx": "^4.19.2", "uglify-js": "^3.19.3" }, - "nodemonConfig": { - "ignore": [ - "**/data/**", - "**/*.json", - ".gitignore" - ], - "delay": 2500 - }, "engines": { "npm": ">=10.8.2" }, diff --git a/src/config/hostsystem.ts b/src/config/hostsystem.ts index 520d3efd..8b4227f4 100644 --- a/src/config/hostsystem.ts +++ b/src/config/hostsystem.ts @@ -1,10 +1,11 @@ +import { RUNNING_IN_DOCKER, VERSION } from "./variables"; import fs from "fs"; import logger from "../utils/logger"; import os from "os"; const userConf = "./src/data/user.conf"; -const inDocker: boolean = !!process.env.RUNNING_IN_DOCKER; -const version: string = process.env.VERSION || "unknown"; +const inDocker: boolean = RUNNING_IN_DOCKER == "true"; +const version: string = VERSION || "unknown"; function writeUserConf() { let previousConfig = null; diff --git a/src/config/variables.ts b/src/config/variables.ts new file mode 100644 index 00000000..26a522be --- /dev/null +++ b/src/config/variables.ts @@ -0,0 +1,24 @@ +import vars from "../data/variables.json"; + +export const { + VERSION, + RUNNING_IN_DOCKER, + TRUSTED_PROXYS, + HA_MASTER, + HA_MASTER_IP, + HA_NODE, + HA_UNSAFE, + DISCORD_WEBHOOK_URL, + EMAIL_SENDER, + EMAIL_RECIPIENT, + EMAIL_PASSWORD, + EMAIL_SERVICE, + PUSHBULLET_ACCESS_TOKEN, + PUSHOVER_USER_KEY, + PUSHOVER_API_TOKEN, + SLACK_WEBHOOK_URL, + TELEGRAM_BOT_TOKEN, + TELEGRAM_CHAT_ID, + WHATSAPP_API_URL, + WHATSAPP_RECIPIENT, +} = vars; diff --git a/src/controllers/fetchData.ts b/src/controllers/fetchData.ts index be9fdc7e..238e8262 100644 --- a/src/controllers/fetchData.ts +++ b/src/controllers/fetchData.ts @@ -22,21 +22,17 @@ const fetchData = async (): Promise => { const allContainerData: AllContainerData = (await fetchAllContainers()) || {}; - if (process.env.OFFLINE === "true") { - logger.info("No new data inserted --- OFFLINE MODE"); - } else { - db.run( - `INSERT INTO data (info) VALUES (?)`, - [JSON.stringify(allContainerData)], - function (error) { - if (error) { - logger.error("Error inserting data:", error); - return; - } - logger.info(`Data inserted with ID: ${this.lastID}`); - }, - ); - } + db.run( + `INSERT INTO data (info) VALUES (?)`, + [JSON.stringify(allContainerData)], + function (error) { + if (error) { + logger.error("Error inserting data:", error); + return; + } + logger.info(`Data inserted with ID: ${this.lastID}`); + }, + ); const containerStatus: AllContainerData = {}; diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index e8557574..f02bde9d 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -3,6 +3,12 @@ import fs from "fs"; import chokidar from "chokidar"; import path from "path"; import { promisify } from "util"; +import { + HA_UNSAFE, + HA_MASTER, + HA_MASTER_IP, + HA_NODE, +} from "../config/variables"; const sleep = promisify(setTimeout); @@ -28,7 +34,7 @@ interface NodeCache { const haMasterPath: string = "./src/data/highAvailability.json"; const haNodePath: string = "./src/data/haNode.json"; const nodeCachePath: string = "./src/data/nodeCache.json"; -const useUnsafeConnection = process.env.HA_UNSAFE || "false"; +const useUnsafeConnection: boolean = HA_UNSAFE == "false"; const lockFilePath: string = "./src/data/ha.lock"; const configFiles: string[] = [ @@ -39,6 +45,7 @@ const configFiles: string[] = [ "./src/data/nodeCache.json", "./src/data/usePassword.txt", "./src/data/password.json", + "./src/data/variables.json", ]; async function acquireLock(): Promise { @@ -119,7 +126,7 @@ async function prepareFilesForSync(): Promise> { async function checkApiReachable(node: string): Promise { let nodeUrl = - useUnsafeConnection === "true" + useUnsafeConnection === true ? `http://${node}/api/status` : `https://${node}/api/status`; @@ -163,7 +170,7 @@ async function synchronizeFilesWithNodes(): Promise { } let nodeUrl = - useUnsafeConnection == "true" + useUnsafeConnection == true ? `http://${node}/ha/sync` : `https://${node}/ha/sync`; @@ -201,8 +208,9 @@ function monitorConfigFiles(): void { } async function startMasterNode() { - if (process.env.HA_MASTER == "true") { - if (!process.env.HA_MASTER_IP) { + let isMaster: boolean = HA_MASTER == "false"; + if (isMaster) { + if (!HA_MASTER_IP) { logger.error( "Master's IP is not set, please set the HA_MASTER_IP variable (example: 10.0.0.4:9876)", ); @@ -213,13 +221,11 @@ async function startMasterNode() { const haConfig: HighAvailabilityConfig = { active: true, master: true, - nodes: process.env.HA_NODE - ? process.env.HA_NODE.split(",").map((node) => node.trim()) - : [], + nodes: HA_NODE ? HA_NODE.split(",").map((node) => node.trim()) : [], }; - const nodeCache: NodeCache = process.env.HA_NODE - ? process.env.HA_NODE.split(",").reduce((cache, node, index) => { + const nodeCache: NodeCache = HA_NODE + ? HA_NODE.split(",").reduce((cache, node, index) => { const [ip, id] = node.trim().split(":"); if (ip && id) { cache[`node${index + 1}`] = { ip, id: parseInt(id, 10) }; diff --git a/src/controllers/notificationController.ts b/src/controllers/notificationController.ts index e34eecda..ad0b1bcc 100644 --- a/src/controllers/notificationController.ts +++ b/src/controllers/notificationController.ts @@ -1,20 +1,29 @@ import notify from "../utils/notifications/_notify"; import logger from "../utils/logger"; +import { + DISCORD_WEBHOOK_URL, + EMAIL_SENDER, + EMAIL_RECIPIENT, + EMAIL_PASSWORD, + EMAIL_SERVICE, + PUSHBULLET_ACCESS_TOKEN, + PUSHOVER_USER_KEY, + PUSHOVER_API_TOKEN, + SLACK_WEBHOOK_URL, + TELEGRAM_BOT_TOKEN, + TELEGRAM_CHAT_ID, + WHATSAPP_API_URL, + WHATSAPP_RECIPIENT, +} from "../config/variables"; const notificationTypes = { - discord: !!process.env.DISCORD_WEBHOOK_URL, - email: !!( - process.env.EMAIL_SENDER && - process.env.EMAIL_RECIPIENT && - process.env.EMAIL_PASSWORD - ), - pushbullet: !!process.env.PUSHBULLET_ACCESS_TOKEN, - pushover: !!(process.env.PUSHOVER_API_TOKEN && process.env.PUSHOVER_USER_KEY), - slack: !!process.env.SLACK_WEBHOOK_UR, - telegram: !!(process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_CHAT_ID), - whatsapp: !!(process.env.WHATSAPP_API_URL && process.env.WHATSAPP_RECIPIENT), - custom: !!process.env.CUSTOM_NOTIFICATION, - customList: process.env.CUSTOM_NOTIFICATION, + discord: !!DISCORD_WEBHOOK_URL, + email: !!(EMAIL_SENDER && EMAIL_RECIPIENT && EMAIL_PASSWORD && EMAIL_SERVICE), + pushbullet: !!PUSHBULLET_ACCESS_TOKEN, + pushover: !!(PUSHOVER_API_TOKEN && PUSHOVER_USER_KEY), + slack: !!SLACK_WEBHOOK_URL, + telegram: !!(TELEGRAM_BOT_TOKEN && TELEGRAM_CHAT_ID), + whatsapp: !!(WHATSAPP_API_URL && WHATSAPP_RECIPIENT), }; async function sendNotification(containerId: string) { @@ -46,17 +55,4 @@ async function sendNotification(containerId: string) { logger.debug(`Sending notification via Pushbullet (${containerId})`); notify("whatsapp", containerId); } - if (notificationTypes.custom) { - const elements: undefined | string[] = notificationTypes.customList - ? notificationTypes.customList.split(",") - : undefined; - if (elements) { - elements.forEach((element) => { - logger.debug(`Sending custom notification ${element} (${containerId})`); - notify(`custom/${element}`, containerId); - }); - } else { - logger.error("Error getting custom notifications"); - } - } } diff --git a/src/controllers/proxy.ts b/src/controllers/proxy.ts index 681adef7..601f1556 100644 --- a/src/controllers/proxy.ts +++ b/src/controllers/proxy.ts @@ -1,8 +1,9 @@ import { Application } from "express"; import logger from "../utils/logger"; +import { TRUSTED_PROXYS } from "../config/variables"; export default function trustedProxies(app: Application) { - const trusted: string = process.env.TRUSTED_PROXYS || ""; + const trusted: string = TRUSTED_PROXYS; if (!trusted) { logger.warn( diff --git a/src/data/template.json b/src/data/template.json new file mode 100644 index 00000000..75e12f22 --- /dev/null +++ b/src/data/template.json @@ -0,0 +1,3 @@ +{ + "text": "{{name}} is {{state}} on {{hostName}}" +} diff --git a/src/init.ts b/src/init.ts index feaa00d1..eb3612bf 100644 --- a/src/init.ts +++ b/src/init.ts @@ -16,6 +16,8 @@ import cors from "cors"; import { blockWhileLocked } from "./middleware/checkLock"; import logger from "./utils/logger"; +const LAB = [limiter, authMiddleware, blockWhileLocked]; + const initializeApp = (app: express.Application): void => { app.use(cors()); app.use(express.json()); @@ -24,21 +26,15 @@ const initializeApp = (app: express.Application): void => { ); swaggerDocs(app as any); - trustedProxies(app); // Configures proxies using CSV string + trustedProxies(app); scheduleFetch(); - app.use("/api", limiter, authMiddleware, blockWhileLocked, api); - app.use("/conf", limiter, authMiddleware, blockWhileLocked, conf); - app.use("/auth", limiter, authMiddleware, blockWhileLocked, auth); - app.use("/data", limiter, authMiddleware, blockWhileLocked, data); - app.use("/frontend", limiter, authMiddleware, blockWhileLocked, frontend); - app.use( - "/notification-service", - limiter, - authMiddleware, - blockWhileLocked, - notificationService, - ); + app.use("/api", LAB, api); + app.use("/conf", LAB, conf); + app.use("/auth", LAB, auth); + app.use("/data", LAB, data); + app.use("/frontend", LAB, frontend); + app.use("/notification-service", LAB, notificationService); app.use("/ha", limiter, authMiddleware, ha); app.get("/", (req: Request, res: Response) => { diff --git a/src/misc/createEnvDev.sh b/src/misc/createEnvDev.sh new file mode 100755 index 00000000..dde36f63 --- /dev/null +++ b/src/misc/createEnvDev.sh @@ -0,0 +1,32 @@ +VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" + +if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then + RUNNING_IN_DOCKER="true" +else + RUNNING_IN_DOCKER="false" +fi + +echo -n "\ +{ + \"VERSION\": \"${VERSION}\", + \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", + \"TRUSTED_PROXYS\": \"${TRUSTED_PROXYS}\", + \"HA_MASTER\": \"${HA_MASTER}\", + \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", + \"HA_NODE\": \"${HA_NODE}\", + \"HA_UNSAFE\": \"${HA_UNSAFE}\", + \"DISCORD_WEBHOOK_URL\": \"${DISCORD_WEBHOOK_URL}\", + \"EMAIL_SENDER\": \"${EMAIL_SENDER}\", + \"EMAIL_RECIPIENT\": \"${EMAIL_RECIPIENT}\", + \"EMAIL_PASSWORD\": \"${EMAIL_PASSWORD}\", + \"EMAIL_SERVICE\": \"${EMAIL_SERVICE}\", + \"PUSHBULLET_ACCESS_TOKEN\": \"${PUSHBULLET_ACCESS_TOKEN}\", + \"PUSHOVER_USER_KEY\": \"${PUSHOVER_USER_KEY}\", + \"PUSHOVER_API_TOKEN\": \"${PUSHOVER_API_TOKEN}\", + \"SLACK_WEBHOOK_URL\": \"${SLACK_WEBHOOK_URL}\", + \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", + \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", + \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", + \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\" +} \ +" > ./src/data/variables.json diff --git a/src/misc/createEnvFile.sh b/src/misc/createEnvFile.sh index cbd8244d..d47eaa9c 100644 --- a/src/misc/createEnvFile.sh +++ b/src/misc/createEnvFile.sh @@ -1,7 +1,7 @@ #!/bin/bash # Version -VERSION="$1" +VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" # Docker if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then @@ -9,9 +9,11 @@ if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then else RUNNING_IN_DOCKER="false" fi -echo " +echo -n "\ { + \"VERSION\": \"${VERSION}\", \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", + \"TRUSTED_PROXYS\": \"${TRUSTED_PROXYS}\", \"HA_MASTER\": \"${HA_MASTER}\", \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", \"HA_NODE\": \"${HA_NODE}\", @@ -28,7 +30,6 @@ echo " \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", - \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\", - \"CUSTOM_NOTIFICATION\": \"${CUSTOM_NOTIFICATION}\" -} -" > /api/src/data/variables.conf + \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\" +} \ +" > /api/src/data/variables.json diff --git a/src/misc/entrypoint.sh b/src/misc/entrypoint.sh index ff5cc617..83eaf46d 100755 --- a/src/misc/entrypoint.sh +++ b/src/misc/entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/bash -VERSION="2.0.0" +VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" echo -e " \033[1;32mWelcome to\033[0m @@ -17,7 +17,6 @@ echo -e " - Documentation: \033[1;32mhttps://outline.itsnik.de/s/dockstat\033[0m - GitHub (Frontend): \033[1;32mhttps://github.com/its4nik/dockstat\033[0m - GitHub (Backend): \033[1;32mhttps://github.com/its4nik/dockstatapi\033[0m -- API Documentation: \033[1;32mhttp://localhost:7000/api-docs\033[0m \033[1;35mSummary:\033[0m @@ -25,6 +24,6 @@ DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simpl " -bash "./createEnvFile.sh" "$VERSION" +bash "./createEnvFile.sh" exec node src/server.js diff --git a/src/routes/getter/routes.ts b/src/routes/getter/routes.ts index c559e637..b6c89c12 100644 --- a/src/routes/getter/routes.ts +++ b/src/routes/getter/routes.ts @@ -134,28 +134,23 @@ router.get("/system", (req: Request, res: Response) => { * description: Error message detailing the issue encountered. */ router.get("/host/:hostName/stats", async (req: Request, res: Response) => { - const {hostName} = req.params; + const { hostName } = req.params; logger.info(`Fetching stats for host: ${hostName}`); - if (process.env.OFFLINE === "true") { - logger.info("Fetching offline Host Stats"); - res.status(200).json(readOfflineLog); - } else { - try { - const docker = getDockerClient(hostName); - const info = await docker.info(); - const version = await docker.version(); - const relevantData = extractRelevantData({ hostName, info, version }); + try { + const docker = getDockerClient(hostName); + const info = await docker.info(); + const version = await docker.version(); + const relevantData = extractRelevantData({ hostName, info, version }); - writeOfflineLog(JSON.stringify(relevantData)); - res.status(200).json(relevantData); - } catch (error: any) { - logger.error( - `Error fetching stats for host: ${hostName} - ${error.message || "Unknown error"}`, - ); - res.status(500).json({ - error: `Error fetching host stats: ${error.message || "Unknown error"}`, - }); - } + writeOfflineLog(JSON.stringify(relevantData)); + res.status(200).json(relevantData); + } catch (error: any) { + logger.error( + `Error fetching stats for host: ${hostName} - ${error.message || "Unknown error"}`, + ); + res.status(500).json({ + error: `Error fetching host stats: ${error.message || "Unknown error"}`, + }); } }); diff --git a/src/utils/notifications/_notify.ts b/src/utils/notifications/_notify.ts index 018b3dce..139a0066 100644 --- a/src/utils/notifications/_notify.ts +++ b/src/utils/notifications/_notify.ts @@ -6,28 +6,6 @@ import { emailNotification } from "./email"; import { whatsappNotification } from "./whatsapp"; import { pushbulletNotification } from "./pushbullet"; import { pushoverNotification } from "./pushover"; -import path from "path"; - -async function loadCustomNotification(scriptPath: string, containerId: string) { - try { - const absolutePath = path.resolve(__dirname, "./custom", scriptPath); - const customModule = await import(absolutePath); - - if (typeof customModule.default !== "function") { - const errorMsg = `The custom notification script at ${scriptPath} does not export a default function.`; - logger.error(errorMsg); - throw new Error(errorMsg); - } - - logger.debug(`Executing custom notification script: ${scriptPath}`); - await customModule.default(containerId); - } catch (error: any) { - logger.error( - `Failed to execute custom notification script (${scriptPath}): ${error.message}`, - ); - throw error; - } -} async function notify(type: string, containerId: string) { if (!containerId) { @@ -35,17 +13,6 @@ async function notify(type: string, containerId: string) { throw new Error("Container ID is required."); } - if (type.startsWith("custom/")) { - const scriptName = type.split("/")[1]; - if (!scriptName) { - const errorMsg = "Custom notification script name is invalid."; - logger.error(errorMsg); - throw new Error(errorMsg); - } - await loadCustomNotification(`${scriptName}.js`, containerId); - return; - } - switch (type) { case "telegram": logger.debug("Sending Telegram notification..."); diff --git a/src/utils/notifications/discord.ts b/src/utils/notifications/discord.ts index 24aaf905..d9be3a02 100644 --- a/src/utils/notifications/discord.ts +++ b/src/utils/notifications/discord.ts @@ -1,8 +1,9 @@ -import * as https from 'https'; +import * as https from "https"; import logger from "../logger"; import { renderTemplate } from "./_template"; +import { DISCORD_WEBHOOK_URL } from "../../config/variables"; -const discord_webhook_url: string | undefined = process.env.DISCORD_WEBHOOK_URL; +const discord_webhook_url: string = DISCORD_WEBHOOK_URL; export async function discordNotification(containerId: string): Promise { const discord_message: string | null = renderTemplate(containerId); @@ -25,28 +26,28 @@ export async function discordNotification(containerId: string): Promise { const options = { hostname: url.hostname, path: url.pathname, - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(postData), + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(postData), }, }; const req = https.request(options, (res) => { - let data = ''; + let data = ""; - res.on('data', (chunk) => { + res.on("data", (chunk) => { data += chunk; }); - res.on('end', () => { + res.on("end", () => { if (res.statusCode !== 200) { logger.error(`Discord API error: ${data}`); } }); }); - req.on('error', (error) => { + req.on("error", (error) => { logger.error("Error sending Discord message:", error); }); diff --git a/src/utils/notifications/email.ts b/src/utils/notifications/email.ts index fbefbab6..57c94ef9 100644 --- a/src/utils/notifications/email.ts +++ b/src/utils/notifications/email.ts @@ -1,11 +1,17 @@ import { SendMailOptions, createTransport } from "nodemailer"; import logger from "../logger"; import { renderTemplate } from "./_template"; +import { + EMAIL_SENDER, + EMAIL_SERVICE, + EMAIL_PASSWORD, + EMAIL_RECIPIENT, +} from "../../config/variables"; -const email_sender: string | undefined = process.env.EMAIL_SENDER; -const email_recipient: string | undefined = process.env.EMAIL_RECIPIENT; -const email_password: string | undefined = process.env.EMAIL_PASSWORD; -const email_service: string | undefined = process.env.EMAIL_SERVICE; +const email_sender: string = EMAIL_SENDER; +const email_recipient: string = EMAIL_RECIPIENT; +const email_password: string = EMAIL_PASSWORD; +const email_service: string = EMAIL_SERVICE; export async function emailNotification(containerId: string) { // Validate email configuration parameters diff --git a/src/utils/notifications/pushbullet.ts b/src/utils/notifications/pushbullet.ts index f008e68c..811427a1 100644 --- a/src/utils/notifications/pushbullet.ts +++ b/src/utils/notifications/pushbullet.ts @@ -1,9 +1,9 @@ import * as https from "https"; import logger from "../logger"; import { renderTemplate } from "./_template"; +import { PUSHBULLET_ACCESS_TOKEN } from "../../config/variables"; -const pushbullet_access_token: string | undefined = - process.env.PUSHBULLET_ACCESS_TOKEN; +const pushbullet_access_token: string = PUSHBULLET_ACCESS_TOKEN; export async function pushbulletNotification( containerId: string, diff --git a/src/utils/notifications/pushover.ts b/src/utils/notifications/pushover.ts index 847c3296..aac71b3b 100644 --- a/src/utils/notifications/pushover.ts +++ b/src/utils/notifications/pushover.ts @@ -1,9 +1,10 @@ -import * as https from 'https'; +import * as https from "https"; import logger from "../logger"; import { renderTemplate } from "./_template"; +import { PUSHOVER_USER_KEY, PUSHOVER_API_TOKEN } from "../../config/variables"; -const pushover_user_key: string | undefined = process.env.PUSHOVER_USER_KEY; -const pushover_api_token: string | undefined = process.env.PUSHOVER_API_TOKEN; +const pushover_user_key: string = PUSHOVER_USER_KEY; +const pushover_api_token: string = PUSHOVER_API_TOKEN; export async function pushoverNotification(containerId: string): Promise { const pushover_message: string | null = renderTemplate(containerId); @@ -24,30 +25,30 @@ export async function pushoverNotification(containerId: string): Promise { }).toString(); const options = { - hostname: 'api.pushover.net', - path: '/1/messages.json', - method: 'POST', + hostname: "api.pushover.net", + path: "/1/messages.json", + method: "POST", headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': Buffer.byteLength(postData), + "Content-Type": "application/x-www-form-urlencoded", + "Content-Length": Buffer.byteLength(postData), }, }; const req = https.request(options, (res) => { - let data = ''; + let data = ""; - res.on('data', (chunk) => { + res.on("data", (chunk) => { data += chunk; }); - res.on('end', () => { + res.on("end", () => { if (res.statusCode !== 200) { logger.error(`Pushover API error: ${data}`); } }); }); - req.on('error', (error) => { + req.on("error", (error) => { logger.error("Error sending Pushover message:", error); }); diff --git a/src/utils/notifications/slack.ts b/src/utils/notifications/slack.ts index b0a8e0b4..e1e7216b 100644 --- a/src/utils/notifications/slack.ts +++ b/src/utils/notifications/slack.ts @@ -1,8 +1,9 @@ -import * as https from 'https'; +import * as https from "https"; import logger from "../logger"; import { renderTemplate } from "./_template"; +import { SLACK_WEBHOOK_URL } from "../../config/variables"; -const slack_webhook_url: string | undefined = process.env.SLACK_WEBHOOK_URL; +const slack_webhook_url: string = SLACK_WEBHOOK_URL; export async function slackNotification(containerId: string): Promise { const slack_message: string | null = renderTemplate(containerId); @@ -25,28 +26,28 @@ export async function slackNotification(containerId: string): Promise { const options = { hostname: url.hostname, path: url.pathname, - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(postData), + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(postData), }, }; const req = https.request(options, (res) => { - let data = ''; + let data = ""; - res.on('data', (chunk) => { + res.on("data", (chunk) => { data += chunk; }); - res.on('end', () => { + res.on("end", () => { if (res.statusCode !== 200) { logger.error(`Slack API error: ${data}`); } }); }); - req.on('error', (error) => { + req.on("error", (error) => { logger.error("Error sending Slack message:", error); }); diff --git a/src/utils/notifications/telegram.ts b/src/utils/notifications/telegram.ts index 174a12e5..440e0916 100644 --- a/src/utils/notifications/telegram.ts +++ b/src/utils/notifications/telegram.ts @@ -1,9 +1,10 @@ -import * as https from 'https'; +import * as https from "https"; import logger from "../logger"; import { renderTemplate } from "./_template"; +import { TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID } from "../../config/variables"; -const telegram_bot_token: string | undefined = process.env.TELEGRAM_BOT_TOKEN; -const telegram_chat_id: string | undefined = process.env.TELEGRAM_CHAT_ID; +const telegram_bot_token: string = TELEGRAM_BOT_TOKEN; +const telegram_chat_id: string = TELEGRAM_CHAT_ID; export async function telegramNotification(containerId: string): Promise { const telegram_message: string | null = renderTemplate(containerId); @@ -23,30 +24,30 @@ export async function telegramNotification(containerId: string): Promise { }); const options = { - hostname: 'api.telegram.org', + hostname: "api.telegram.org", path: `/bot${telegram_bot_token}/sendMessage`, - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(postData), + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(postData), }, }; const req = https.request(options, (res) => { - let data = ''; + let data = ""; - res.on('data', (chunk) => { + res.on("data", (chunk) => { data += chunk; }); - res.on('end', () => { + res.on("end", () => { if (res.statusCode !== 200) { logger.error(`Telegram API error: ${data}`); } }); }); - req.on('error', (error) => { + req.on("error", (error) => { logger.error("Error sending message:", error); }); diff --git a/src/utils/notifications/whatsapp.ts b/src/utils/notifications/whatsapp.ts index 178f6d53..1eb7575e 100644 --- a/src/utils/notifications/whatsapp.ts +++ b/src/utils/notifications/whatsapp.ts @@ -1,9 +1,10 @@ -import * as https from 'https'; +import * as https from "https"; import logger from "../logger"; import { renderTemplate } from "./_template"; +import { WHATSAPP_API_URL, WHATSAPP_RECIPIENT } from "../../config/variables"; -const whatsapp_api_url: string | undefined = process.env.WHATSAPP_API_URL; -const whatsapp_recipient: string | undefined = process.env.WHATSAPP_RECIPIENT; +const whatsapp_api_url: string = WHATSAPP_API_URL; +const whatsapp_recipient: string = WHATSAPP_RECIPIENT; export async function whatsappNotification(containerId: string): Promise { const whatsapp_message: string | null = renderTemplate(containerId); @@ -27,28 +28,28 @@ export async function whatsappNotification(containerId: string): Promise { const options = { hostname: url.hostname, path: url.pathname, - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(postData), + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(postData), }, }; const req = https.request(options, (res) => { - let data = ''; + let data = ""; - res.on('data', (chunk) => { + res.on("data", (chunk) => { data += chunk; }); - res.on('end', () => { + res.on("end", () => { if (res.statusCode !== 200) { logger.error(`WhatsApp API error: ${data}`); } }); }); - req.on('error', (error) => { + req.on("error", (error) => { logger.error("Error sending WhatsApp message:", error); }); diff --git a/tsconfig.json b/tsconfig.json index 8fc3c320..4af6b1d1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "resolveJsonModule": true, "target": "ES2020", "outDir": "dist/src", "module": "CommonJS", @@ -11,11 +12,6 @@ }, "$schema": "https://json.schemastore.org/tsconfig", "display": "Recommended", - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "**/*.spec.ts" - ] -} \ No newline at end of file + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.spec.ts"] +} From 39445445b7a7c14c59905bb6a3402015ba7fbc20 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 25 Dec 2024 20:17:23 +0100 Subject: [PATCH 038/369] Fix: update .gitignore --- .gitignore | 1 + TODO.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c7f5c64e..ee4e7afe 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ src/data/user.conf src/data/password.json src/data/ha.lock src/data/frontendConfiguration.json +src/data/variables.json docker .test* diff --git a/TODO.md b/TODO.md index d2659e2d..c4687d72 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,4 @@ -- [ ] Better Offline mode using "faker" library or self written (probably self written) +- [X] ~Better Offline mode using "faker" library or self written (probably self written)~ Not needed since there is a docker-compsoe file for local testing integrated inside the repo - [X] HA compatibility - [X] !!! Needs testing !!! Add automatic notifications when container state changes, according to selected level for notification service - [ ] Image update and update notifications @@ -10,3 +10,4 @@ - [ ] Websockets - [X] Better /api/status endpoint with connection status of each host - [X] Update notification service +- [X] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) From b6338e9270b136eb21f6b652af39e1cc73f6885e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 25 Dec 2024 23:37:29 +0100 Subject: [PATCH 039/369] Chore: Creating default files on container startup Fix: Fixing env variables Docker: Added dev Dockerfile without Swagger docs --- Dockerfile-dev | 61 +++++++++++++++++++++++++++++ docker-compose.yaml | 21 ++++++++-- package.json | 4 +- src/config/db.ts | 10 ++--- src/config/hostsystem.ts | 3 +- src/config/initFiles.ts | 41 +++++++++++++++++++ src/config/loggerConfig.ts | 1 + src/controllers/highAvailability.ts | 18 ++++----- src/init.ts | 2 + 9 files changed, 138 insertions(+), 23 deletions(-) create mode 100644 Dockerfile-dev create mode 100644 src/config/initFiles.ts diff --git a/Dockerfile-dev b/Dockerfile-dev new file mode 100644 index 00000000..7ad56f09 --- /dev/null +++ b/Dockerfile-dev @@ -0,0 +1,61 @@ +# Stage 1: Build stage +FROM node:alpine AS builder + +LABEL maintainer="https://github.com/its4nik" +LABEL version="2.0.1" +LABEL description="API for DockStat" +LABEL license="BSD-3-Clause license" +LABEL repository="https://github.com/its4nik/dockstatapi" +LABEL documentation="https://github.com/its4nik/dockstatapi" +LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" +LABEL org.opencontainers.image.licenses="BSD-3-Clause license" +LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" + +WORKDIR /build +ENV NODE_NO_WARNINGS=1 + +RUN apk update && \ + apk upgrade && \ + apk add bash + + +COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +RUN npm install + +COPY ./src ./src +RUN npm run build + +# Stage 2: main stage +FROM alpine AS main + +# Needed packages +RUN apk update && \ + apk upgrade && \ + apk add --update npm + +WORKDIR /build + +RUN mkdir -p /build/src/data + +COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +RUN npm install --omit=dev + +COPY --from=builder /build/dist/* /build/src +COPY --from=builder /build/src/misc/entrypoint.sh /build/entrypoint.sh +COPY --from=builder /build/src/misc/createEnvFile.sh /build/createEnvFile.sh + +RUN node src/config/db.js + +# Stage 3: Production stage +FROM alpine AS production + +RUN apk add --update bash curl nodejs +HEALTHCHECK --interval=5m --timeout=3s \ + CMD curl -f http://localhost:9876/api/status || exit 1 + +WORKDIR /api + +COPY --from=main /build /api + +EXPOSE 9876 +ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/docker-compose.yaml b/docker-compose.yaml index 31586575..06d1f459 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,17 +1,26 @@ +networks: + shared-network: + driver: bridge + services: master: container_name: master environment: - NODE_ENV=development - HA_MASTER=true - - HA_MASTER_IP=127.0.0.1:9876 - - HA_NODE=127.0.0.1:6789 + - HA_MASTER_IP=master:9876 + - HA_NODE=slave:9876 - HA_UNSAFE=true volumes: - ./docker/master:/api/src/data ports: - 9876:9876 image: dockstatapi:local + networks: + - shared-network + depends_on: + - slave + - test-socket-proxy slave: container_name: slave environment: @@ -19,12 +28,14 @@ services: volumes: - ./docker/slave:/api/src/data ports: - - 6789:6789 + - 6789:9876 image: dockstatapi:local + networks: + - shared-network test-socket-proxy: image: lscr.io/linuxserver/socket-proxy:latest - container_name: socket-proxy + container_name: test-socket-proxy environment: - ALLOW_START=1 #optional - ALLOW_STOP=1 #optional @@ -59,3 +70,5 @@ services: read_only: true tmpfs: - /run + networks: + - shared-network diff --git a/package.json b/package.json index d600a792..eb9f8652 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ "build": "npx tsc", "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", "mini": "bash ./src/misc/minifyDist.sh", - "docker": "sudo docker compose up -d", + "docker": "docker compose up -d", "docker:full": "docker compose up -d && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", - "docker:build": "docker build . -t \"dockstatapi:local\" && docker compose up -d", + "docker:build": "docker build . -t \"dockstatapi:local\" -f ./Dockerfile-dev && docker compose up -d", "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down" }, "keywords": [], diff --git a/src/config/db.ts b/src/config/db.ts index 93972131..80861350 100644 --- a/src/config/db.ts +++ b/src/config/db.ts @@ -15,12 +15,10 @@ const db: sqlite3.Database = new sqlite3.Database( info TEXT NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )`, - (tableErr: Error | null) => { - if (tableErr) { - logger.error("Error creating table:", tableErr.message); - } else { - logger.info("Database created / opened successfully"); - } + () => { + logger.info( + "Database created / opened successfully, table is ready.", + ); }, ); } diff --git a/src/config/hostsystem.ts b/src/config/hostsystem.ts index 8b4227f4..91e44ed5 100644 --- a/src/config/hostsystem.ts +++ b/src/config/hostsystem.ts @@ -53,9 +53,8 @@ function writeUserConf() { backendVersion: version, }; - logger.info("Starting the server..."); logger.info( - `At: ${startDetails.startedAt} - Version: ${startDetails.backendVersion} - Docker: ${installationDetails.inDocker} - Installed as: ${installationDetails.installedBy} - Platform: ${installationDetails.platform} - Arch: ${installationDetails.arch}`, + `Starting at: ${startDetails.startedAt} - Version: ${startDetails.backendVersion} - Docker: ${installationDetails.inDocker} - Installed as: ${installationDetails.installedBy} - Platform: ${installationDetails.platform} - Arch: ${installationDetails.arch}`, ); } diff --git a/src/config/initFiles.ts b/src/config/initFiles.ts new file mode 100644 index 00000000..1f8776a6 --- /dev/null +++ b/src/config/initFiles.ts @@ -0,0 +1,41 @@ +import { writeFileSync, existsSync } from "fs"; +import logger from "../utils/logger"; +import path from "path"; + +const files = [ + { + path: "./src/data/password.json", + content: JSON.stringify( + { + hash: "", + salt: "", + }, + null, + 2, + ), + }, + { path: "./src/data/states.json", content: "{}" }, + { + path: "./src/data/template.json", + content: JSON.stringify( + { text: "{{name}} is {{state}} on {{hostName}}" }, + null, + 2, + ), + }, + { path: "./src/data/frontendConfiguration.json", content: "[]" }, + { path: "./src/data/usePassword.txt", content: "false" }, +]; + +function initFiles(): void { + files.forEach(({ path: filePath, content }) => { + if (!existsSync(filePath)) { + writeFileSync(filePath, content); + logger.info(`Created: ${filePath}`); + } else { + logger.debug(`Skipped (already exists): ${filePath}`); + } + }); +} + +export default initFiles; diff --git a/src/config/loggerConfig.ts b/src/config/loggerConfig.ts index 5d1a33e4..45feb5c7 100644 --- a/src/config/loggerConfig.ts +++ b/src/config/loggerConfig.ts @@ -7,6 +7,7 @@ const red = "\x1b[31m"; const green = "\x1b[32m"; const yellow = "\x1b[33m"; const blue = "\x1b[34m"; +const pink = "\x1b[38;5;213m"; // Pink color for sync logs const ignoreExitListenerLogs = format((info) => { if ( diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index f02bde9d..919148e4 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -34,7 +34,7 @@ interface NodeCache { const haMasterPath: string = "./src/data/highAvailability.json"; const haNodePath: string = "./src/data/haNode.json"; const nodeCachePath: string = "./src/data/nodeCache.json"; -const useUnsafeConnection: boolean = HA_UNSAFE == "false"; +const useUnsafeConnection: boolean = JSON.parse(HA_UNSAFE || "false"); const lockFilePath: string = "./src/data/ha.lock"; const configFiles: string[] = [ @@ -45,7 +45,6 @@ const configFiles: string[] = [ "./src/data/nodeCache.json", "./src/data/usePassword.txt", "./src/data/password.json", - "./src/data/variables.json", ]; async function acquireLock(): Promise { @@ -130,6 +129,8 @@ async function checkApiReachable(node: string): Promise { ? `http://${node}/api/status` : `https://${node}/api/status`; + logger.info(`Checking node (${nodeUrl}) reachability`); + try { const response = await fetch(nodeUrl); if (!response.ok) { @@ -138,7 +139,7 @@ async function checkApiReachable(node: string): Promise { } const data = await response.json(); - if (data.ApiReachable) { + if (data.ApiReachable as boolean) { logger.info(`Node ${node} is reachable.`); return true; } else { @@ -208,15 +209,14 @@ function monitorConfigFiles(): void { } async function startMasterNode() { - let isMaster: boolean = HA_MASTER == "false"; - if (isMaster) { + if (HA_MASTER == "true") { if (!HA_MASTER_IP) { logger.error( "Master's IP is not set, please set the HA_MASTER_IP variable (example: 10.0.0.4:9876)", ); } else { const haNodeConfig: HaNodeConfig = { - master: "HA_MASTER_IP", + master: HA_MASTER_IP, }; const haConfig: HighAvailabilityConfig = { active: true, @@ -226,9 +226,9 @@ async function startMasterNode() { const nodeCache: NodeCache = HA_NODE ? HA_NODE.split(",").reduce((cache, node, index) => { - const [ip, id] = node.trim().split(":"); - if (ip && id) { - cache[`node${index + 1}`] = { ip, id: parseInt(id, 10) }; + const [ip, port] = node.trim().split(":"); + if (ip && port) { + cache[`node-${index + 1}`] = { ip, id: parseInt(port, 10) }; } return cache; }, {} as NodeCache) diff --git a/src/init.ts b/src/init.ts index eb3612bf..119950c0 100644 --- a/src/init.ts +++ b/src/init.ts @@ -15,10 +15,12 @@ import { scheduleFetch } from "./controllers/scheduler"; import cors from "cors"; import { blockWhileLocked } from "./middleware/checkLock"; import logger from "./utils/logger"; +import initFiles from "./config/initFiles"; const LAB = [limiter, authMiddleware, blockWhileLocked]; const initializeApp = (app: express.Application): void => { + initFiles(); app.use(cors()); app.use(express.json()); app.use("/api-docs", (req: Request, res: Response, next: NextFunction) => From ccabc0cea19b18a936b17d195276ac2663a518f6 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 25 Dec 2024 23:38:13 +0100 Subject: [PATCH 040/369] Fix: Replacing all single file names with one folder --- .gitignore | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index ee4e7afe..6c617861 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,5 @@ # custom paths: -src/data/database.db -src/data/dockerConfig.json -src/data/highAvailability.json -src/data/states.json -src/data/user.conf -src/data/password.json -src/data/ha.lock -src/data/frontendConfiguration.json -src/data/variables.json +src/data/* docker .test* From d9c600abd8a36c696c247d45f11147cf4c8ea55a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 25 Dec 2024 23:49:09 +0100 Subject: [PATCH 041/369] Fix: Adding sample variables.json to docker container before building --- .dockerignore | 150 ++++++++++++++++++++++++++++++++++++++- Dockerfile | 1 + Dockerfile-dev | 1 + src/sample-variable.json | 22 ++++++ 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/sample-variable.json diff --git a/.dockerignore b/.dockerignore index 10b44aec..2d993096 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,150 @@ +# custom paths: +src/data/* +*.md *.txt -*.md \ No newline at end of file +docker +.test* +# Created by https://www.toptal.com/developers/gitignore/api/node +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/Dockerfile b/Dockerfile index 53f3b729..26f492b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,7 @@ COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ RUN npm install COPY ./src ./src +RUN mv ./src/sample-variable.json ./src/data/variables.json RUN npm run build:mini # Stage 2: main stage diff --git a/Dockerfile-dev b/Dockerfile-dev index 7ad56f09..6e9452a0 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -23,6 +23,7 @@ COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ RUN npm install COPY ./src ./src +RUN mv ./src/sample-variable.json ./src/data/variables.json RUN npm run build # Stage 2: main stage diff --git a/src/sample-variable.json b/src/sample-variable.json new file mode 100644 index 00000000..06153af5 --- /dev/null +++ b/src/sample-variable.json @@ -0,0 +1,22 @@ +{ + "VERSION": "", + "RUNNING_IN_DOCKER": "", + "TRUSTED_PROXYS": "", + "HA_MASTER": "", + "HA_MASTER_IP": "", + "HA_NODE": "", + "HA_UNSAFE": "", + "DISCORD_WEBHOOK_URL": "", + "EMAIL_SENDER": "", + "EMAIL_RECIPIENT": "", + "EMAIL_PASSWORD": "", + "EMAIL_SERVICE": "", + "PUSHBULLET_ACCESS_TOKEN": "", + "PUSHOVER_USER_KEY": "", + "PUSHOVER_API_TOKEN": "", + "SLACK_WEBHOOK_URL": "", + "TELEGRAM_BOT_TOKEN": "", + "TELEGRAM_CHAT_ID": "", + "WHATSAPP_API_URL": "", + "WHATSAPP_RECIPIENT": "" +} From c82473ecda7b2fca0e5697cd8bc0fa76e517800a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 10:16:24 +0100 Subject: [PATCH 042/369] Fix: Add error handling for malformed input data in the reduce function Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/utils/extractHostData.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/utils/extractHostData.ts b/src/utils/extractHostData.ts index b6192ea7..65577cce 100644 --- a/src/utils/extractHostData.ts +++ b/src/utils/extractHostData.ts @@ -43,13 +43,23 @@ function extractRelevantData(jsonData: JsonData) { NCPU: jsonData.info.NCPU, }, version: { - Components: jsonData.version.Components.reduce( - (acc, component) => { - acc[component.Name] = component.Version; - return acc; - }, - {}, - ), + Components: (() => { + try { + if (!Array.isArray(jsonData?.version?.Components)) { + return {}; + } + + return jsonData.version.Components.reduce((acc, component) => { + if (typeof component?.Name === 'string' && typeof component?.Version === 'string') { + acc[component.Name] = component.Version; + } + return acc; + }, {}); + } catch (error) { + console.error('Error processing Components data:', error); + return {}; + } + })(), }, }; } From 35630f46d8fe6f0c017877f928e459fdc6ef39ad Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 18:47:22 +0100 Subject: [PATCH 043/369] Fix: Add rate limiting to file read (auth) --- src/middleware/authMiddleware.ts | 3 ++- src/utils/rateLimitReadFile.ts | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/utils/rateLimitReadFile.ts diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts index 8caad081..a50fcda5 100644 --- a/src/middleware/authMiddleware.ts +++ b/src/middleware/authMiddleware.ts @@ -2,6 +2,7 @@ import bcrypt from "bcrypt"; import fs from "fs"; import { Request, Response, NextFunction } from "express"; import logger from "../utils/logger"; +import { rateLimitedReadFile } from "../utils/rateLimitReadFile"; const passwordFile = "./src/data/password.json"; const passwordBool = "./src/data/usePassword.txt"; @@ -28,7 +29,7 @@ async function authMiddleware( return; } - const passwordData = await fs.promises.readFile(passwordFile, "utf8"); + const passwordData = await rateLimitedReadFile(passwordFile); const storedData = JSON.parse(passwordData); const passwordMatch = await bcrypt.compare( diff --git a/src/utils/rateLimitReadFile.ts b/src/utils/rateLimitReadFile.ts new file mode 100644 index 00000000..415cddef --- /dev/null +++ b/src/utils/rateLimitReadFile.ts @@ -0,0 +1,22 @@ +import { promises as fs } from "fs"; + +const delay = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +let lastReadTime = 0; +const rateLimitDuration = 500; + +export const rateLimitedReadFile = async ( + filePath: string, + encoding: BufferEncoding = "utf8", +): Promise => { + const now = Date.now(); + const timeSinceLastRead = now - lastReadTime; + + if (timeSinceLastRead < rateLimitDuration) { + await delay(rateLimitDuration - timeSinceLastRead); + } + + lastReadTime = Date.now(); + return fs.readFile(filePath, encoding); +}; From f81a96cc0bab405d4ee802ecaa4828a1689d9fdc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 18:58:03 +0100 Subject: [PATCH 044/369] Fix: Add rate limiting FS functions --- src/middleware/authMiddleware.ts | 5 ++--- src/middleware/checkLock.ts | 8 +++---- src/utils/rateLimitFS.ts | 36 ++++++++++++++++++++++++++++++++ src/utils/rateLimitReadFile.ts | 22 ------------------- 4 files changed, 42 insertions(+), 29 deletions(-) create mode 100644 src/utils/rateLimitFS.ts delete mode 100644 src/utils/rateLimitReadFile.ts diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts index a50fcda5..08ffd219 100644 --- a/src/middleware/authMiddleware.ts +++ b/src/middleware/authMiddleware.ts @@ -1,8 +1,7 @@ import bcrypt from "bcrypt"; -import fs from "fs"; import { Request, Response, NextFunction } from "express"; import logger from "../utils/logger"; -import { rateLimitedReadFile } from "../utils/rateLimitReadFile"; +import { rateLimitedReadFile } from "../utils/rateLimitFS"; const passwordFile = "./src/data/password.json"; const passwordBool = "./src/data/usePassword.txt"; @@ -13,7 +12,7 @@ async function authMiddleware( next: NextFunction, ): Promise { try { - const authStatusData = await fs.promises.readFile(passwordBool, "utf8"); + const authStatusData = await rateLimitedReadFile(passwordBool); const isAuthEnabled = authStatusData.trim() === "true"; if (!isAuthEnabled) { diff --git a/src/middleware/checkLock.ts b/src/middleware/checkLock.ts index 747889dc..73740a07 100644 --- a/src/middleware/checkLock.ts +++ b/src/middleware/checkLock.ts @@ -1,14 +1,14 @@ -import fs from "fs"; import { Request, Response, NextFunction } from "express"; +import { rateLimitedExistsSync } from "../utils/rateLimitFS"; const lockFilePath = "./src/data/ha.lock"; -export function blockWhileLocked( +export async function blockWhileLocked( req: Request, res: Response, next: NextFunction, -): void { - if (fs.existsSync(lockFilePath)) { +): Promise { + if (await rateLimitedExistsSync(lockFilePath)) { res.status(503).json({ error: "Service unavailable. The high-availability lock is currently active. Please try again later.", diff --git a/src/utils/rateLimitFS.ts b/src/utils/rateLimitFS.ts new file mode 100644 index 00000000..a8f0b42d --- /dev/null +++ b/src/utils/rateLimitFS.ts @@ -0,0 +1,36 @@ +import { promises as fs, existsSync } from "fs"; + +const delay = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +let lastOperationTime = 0; +const rateLimitDuration = 500; + +export const rateLimitedReadFile = async ( + filePath: string, + encoding: BufferEncoding = "utf8", +): Promise => { + const now = Date.now(); + const timeSinceLastOperation = now - lastOperationTime; + + if (timeSinceLastOperation < rateLimitDuration) { + await delay(rateLimitDuration - timeSinceLastOperation); + } + + lastOperationTime = Date.now(); + return fs.readFile(filePath, encoding); +}; + +export const rateLimitedExistsSync = async ( + filePath: string, +): Promise => { + const now = Date.now(); + const timeSinceLastOperation = now - lastOperationTime; + + if (timeSinceLastOperation < rateLimitDuration) { + await delay(rateLimitDuration - timeSinceLastOperation); + } + + lastOperationTime = Date.now(); + return existsSync(filePath); +}; diff --git a/src/utils/rateLimitReadFile.ts b/src/utils/rateLimitReadFile.ts deleted file mode 100644 index 415cddef..00000000 --- a/src/utils/rateLimitReadFile.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { promises as fs } from "fs"; - -const delay = (ms: number): Promise => - new Promise((resolve) => setTimeout(resolve, ms)); - -let lastReadTime = 0; -const rateLimitDuration = 500; - -export const rateLimitedReadFile = async ( - filePath: string, - encoding: BufferEncoding = "utf8", -): Promise => { - const now = Date.now(); - const timeSinceLastRead = now - lastReadTime; - - if (timeSinceLastRead < rateLimitDuration) { - await delay(rateLimitDuration - timeSinceLastRead); - } - - lastReadTime = Date.now(); - return fs.readFile(filePath, encoding); -}; From 8b9493fff416963f97ed152c4514267855d8c434 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 19:16:01 +0100 Subject: [PATCH 045/369] Feat: Advanced codeql.yml --- .github/workflows/codeql.yml | 52 ++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..695a6087 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,52 @@ +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '32 1 * * 5' + +jobs: + analyze: + name: Analyze TypeScript + runs-on: 'ubuntu-latest' + permissions: + security-events: write + packages: read + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + queries: security-extended + + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From 8165b3e7ac456b6074451a237db46c0343b37b18 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 19:47:17 +0100 Subject: [PATCH 046/369] Chore: Add more workflows for validation/licensing/... --- .github/workflows/Licensed.yml | 12 + .github/workflows/remove-stale.yml | 17 + .github/workflows/validation.yml | 58 + eslint.config.mjs | 12 + package-lock.json | 1359 +++++++++++++++++++++- package.json | 20 +- src/controllers/frontendConfiguration.ts | 50 +- src/controllers/highAvailability.ts | 4 +- src/routes/getter/routes.ts | 2 +- src/utils/connectionChecker.ts | 1 - src/utils/containerService.ts | 12 +- src/utils/extractHostData.ts | 20 +- 12 files changed, 1516 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/Licensed.yml create mode 100644 .github/workflows/remove-stale.yml create mode 100644 .github/workflows/validation.yml create mode 100644 eslint.config.mjs diff --git a/.github/workflows/Licensed.yml b/.github/workflows/Licensed.yml new file mode 100644 index 00000000..e6475a78 --- /dev/null +++ b/.github/workflows/Licensed.yml @@ -0,0 +1,12 @@ +name: Licensed + +on: + workflow_call: + +jobs: + validate-cached-dependency-records: + runs-on: ubuntu-latest + name: Check licenses + steps: + - name: Licensed CI + uses: github/licensed-ci@v1.11.1 diff --git a/.github/workflows/remove-stale.yml b/.github/workflows/remove-stale.yml new file mode 100644 index 00000000..ccccef97 --- /dev/null +++ b/.github/workflows/remove-stale.yml @@ -0,0 +1,17 @@ +name: "Close stale issues and PR" +on: + schedule: + - cron: "30 1 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." + stale-pr-message: "This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days." + close-issue-message: "This issue was closed because it has been stalled for 5 days with no activity." + days-before-stale: 30 + days-before-close: 5 + days-before-pr-close: -1 diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml new file mode 100644 index 00000000..7dcf706a --- /dev/null +++ b/.github/workflows/validation.yml @@ -0,0 +1,58 @@ +name: Basic validation + +on: + workflow_call: + inputs: + operating-systems: + description: "Optional input to set a list of operating systems which the workflow uses. Defaults to ['ubuntu-latest', 'windows-latest', 'macos-latest'] if not set" + required: false + type: string + default: "['ubuntu-latest', 'windows-latest', 'macos-latest']" + enable-audit: + description: "Optional input to enable npm package audit process" + required: false + type: boolean + default: true + node-version: + description: "Optional input to set the version of Node.js used to build the project. The input syntax corresponds to the setup-node's one" + required: false + type: string + default: "20.x" + node-caching: + description: "Optional input to set up caching for the setup-node action. The input syntax corresponds to the setup-node's one. Set to an empty string if caching isn't needed" + required: false + type: string + default: "npm" + +jobs: + build: + runs-on: ${{matrix.operating-systems}} + strategy: + fail-fast: false + matrix: + operating-systems: ${{fromJson(inputs.operating-systems)}} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js ${{inputs.node-version}} + uses: actions/setup-node@v4 + with: + node-version: ${{inputs.node-version}} + cache: ${{inputs.node-caching}} + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Run prettier + run: npm run pettier + + - name: Run linter + run: npm run lint + + - name: Build + run: npm run build:mini + + - name: Audit packages + run: npm audit --audit-level=high + if: ${{inputs.enable-audit}} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..5b7b70a1 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,12 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + { ignores: ["node_modules/*", "dist/*"] }, + { files: ["src/*.{ts}"] }, + { languageOptions: { globals: globals.node } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; diff --git a/package-lock.json b/package-lock.json index 27899c7b..118aa2bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dockstatapi", - "version": "2.0.0", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dockstatapi", - "version": "2.0.0", + "version": "2.0.1", "license": "BSD 3-Clause License", "dependencies": { "bcrypt": "^5.1.1", @@ -25,6 +25,7 @@ "winston": "^3.15.0" }, "devDependencies": { + "@eslint/js": "^9.17.0", "@playwright/test": "^1.49.0", "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", @@ -37,11 +38,17 @@ "@types/supports-color": "^8.1.3", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.7", + "@typescript-eslint/eslint-plugin": "^8.18.2", + "@typescript-eslint/parser": "^8.18.2", "dependency-cruiser": "^16.5.0", + "eslint": "^9.17.0", + "globals": "^15.14.0", "nodemon": "^3.1.7", "ora": "^8.1.1", + "prettier": "^3.4.2", "ts-node": "^10.9.2", "tsx": "^4.19.2", + "typescript-eslint": "^8.18.2", "uglify-js": "^3.19.3" }, "engines": { @@ -539,6 +546,180 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.5", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", + "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", + "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", + "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -546,6 +727,72 @@ "license": "MIT", "optional": true }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -620,6 +867,44 @@ } } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -771,6 +1056,13 @@ "@types/ssh2": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/express": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", @@ -950,6 +1242,235 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.2.tgz", + "integrity": "sha512-adig4SzPLjeQ0Tm+jvsozSGiCliI2ajeURDGHjZ2llnA+A67HihCQ+a3amtPhUakd1GlwHxSRvzOZktbEvhPPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.18.2", + "@typescript-eslint/type-utils": "8.18.2", + "@typescript-eslint/utils": "8.18.2", + "@typescript-eslint/visitor-keys": "8.18.2", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.2.tgz", + "integrity": "sha512-y7tcq4StgxQD4mDr9+Jb26dZ+HTZ/SkfqpXSiqeUXZHxOUyjWDKsmwKhJ0/tApR08DgOhrFAoAhyB80/p3ViuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.18.2", + "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/typescript-estree": "8.18.2", + "@typescript-eslint/visitor-keys": "8.18.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.2.tgz", + "integrity": "sha512-YJFSfbd0CJjy14r/EvWapYgV4R5CHzptssoag2M7y3Ra7XNta6GPAJPPP5KGB9j14viYXyrzRO5GkX7CRfo8/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/visitor-keys": "8.18.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.2.tgz", + "integrity": "sha512-AB/Wr1Lz31bzHfGm/jgbFR0VB0SML/hd2P1yxzKDM48YmP7vbyJNHRExUE/wZsQj2wUCvbWH8poNHFuxLqCTnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.18.2", + "@typescript-eslint/utils": "8.18.2", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.2.tgz", + "integrity": "sha512-Z/zblEPp8cIvmEn6+tPDIHUbRu/0z5lqZ+NvolL5SvXWT5rQy7+Nch83M0++XzO0XrWRFWECgOAyE8bsJTl1GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.2.tgz", + "integrity": "sha512-WXAVt595HjpmlfH4crSdM/1bcsqh+1weFRWIa9XMTx/XHZ9TCKMcr725tLYqWOgzKdeDrqVHxFotrvWcEsk2Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/visitor-keys": "8.18.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.2.tgz", + "integrity": "sha512-Cr4A0H7DtVIPkauj4sTSXVl+VBWewE9/o40KcF3TV9aqDEOWoXF3/+oRXNby3DYzZeCATvbdksYsGZzplwnK/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.18.2", + "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/typescript-estree": "8.18.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.2.tgz", + "integrity": "sha512-zORcwn4C3trOWiCqFQP1x6G3xTRyZ1LYydnj51cRnJ6hxBlr/cKPckk+PKPUw/fXmvfKTcw7bwY3w9izgx5jZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.18.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1467,6 +1988,16 @@ "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", "license": "MIT" }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -1702,6 +2233,21 @@ "dev": true, "license": "MIT" }, + "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/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -1752,6 +2298,13 @@ "node": ">=4.0.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2074,6 +2627,276 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", + "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.17.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/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/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/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/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2184,6 +3007,37 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", @@ -2191,6 +3045,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/fastq": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -2220,6 +3084,19 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -2272,6 +3149,44 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", @@ -2496,6 +3411,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globals": { + "version": "15.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", + "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2515,6 +3443,13 @@ "devOptional": true, "license": "ISC" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -2683,12 +3618,29 @@ "dev": true, "license": "ISC" }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.8.19" } @@ -2926,8 +3878,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC", - "optional": true + "devOptional": true, + "license": "ISC" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -2948,6 +3900,13 @@ "license": "MIT", "optional": true }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -2955,6 +3914,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2968,6 +3934,16 @@ "node": ">=6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -2984,6 +3960,36 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -2996,6 +4002,13 @@ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", @@ -3155,6 +4168,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -3164,6 +4187,33 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -3375,6 +4425,13 @@ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -3718,6 +4775,24 @@ "license": "MIT", "peer": true }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/ora/-/ora-8.1.1.tgz", @@ -3796,6 +4871,38 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -3812,6 +4919,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3821,6 +4941,16 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -3830,6 +4960,16 @@ "node": ">=0.10.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-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -3921,6 +5061,32 @@ "node": ">=10" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -3995,6 +5161,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -4010,6 +5186,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4133,6 +5330,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -4183,6 +5390,17 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4199,6 +5417,30 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4348,6 +5590,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/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -4887,6 +6152,19 @@ "node": ">= 14.0.0" } }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -5055,6 +6333,19 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "license": "Unlicense" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -5083,6 +6374,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.2.tgz", + "integrity": "sha512-KuXezG6jHkvC3MvizeXgupZzaG5wjhU3yE8E7e6viOvAvD9xAWYp8/vy0WULTGe9DYDWcQu7aW03YIV3mSitrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.18.2", + "@typescript-eslint/parser": "8.18.2", + "@typescript-eslint/utils": "8.18.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -5139,6 +6453,16 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5221,8 +6545,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, "license": "ISC", - "optional": true, "dependencies": { "isexe": "^2.0.0" }, @@ -5278,6 +6602,16 @@ "node": ">= 12.0.0" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -5309,6 +6643,19 @@ "node": ">=6" } }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/z-schema": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", diff --git a/package.json b/package.json index eb9f8652..20af78a8 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,10 @@ "docker": "docker compose up -d", "docker:full": "docker compose up -d && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", "docker:build": "docker build . -t \"dockstatapi:local\" -f ./Dockerfile-dev && docker compose up -d", - "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down" + "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", + "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write", + "lint": "npx eslint", + "lint:fix": "npx eslint --fix" }, "keywords": [], "author": "Its4Nik", @@ -39,23 +42,30 @@ "winston": "^3.15.0" }, "devDependencies": { - "@types/dockerode": "^3.3.31", - "@types/supports-color": "^8.1.3", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.7", + "@eslint/js": "^9.17.0", "@playwright/test": "^1.49.0", "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", + "@types/dockerode": "^3.3.31", "@types/express": "^5.0.0", "@types/express-handlebars": "^5.3.1", "@types/node": "^22.9.0", "@types/node-fetch": "^2.6.12", "@types/nodemailer": "^6.4.17", + "@types/supports-color": "^8.1.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.7", + "@typescript-eslint/eslint-plugin": "^8.18.2", + "@typescript-eslint/parser": "^8.18.2", "dependency-cruiser": "^16.5.0", + "eslint": "^9.17.0", + "globals": "^15.14.0", "nodemon": "^3.1.7", "ora": "^8.1.1", + "prettier": "^3.4.2", "ts-node": "^10.9.2", "tsx": "^4.19.2", + "typescript-eslint": "^8.18.2", "uglify-js": "^3.19.3" }, "engines": { diff --git a/src/controllers/frontendConfiguration.ts b/src/controllers/frontendConfiguration.ts index e83eaaee..4d31943e 100644 --- a/src/controllers/frontendConfiguration.ts +++ b/src/controllers/frontendConfiguration.ts @@ -9,7 +9,7 @@ const regex = new RegExp(expression); // Hide Containers: async function hideContainer(containerName: string) { try { - let data = await readData(); + const data = await readData(); const containerIndex = data.findIndex( (container: any) => container.name === containerName, ); @@ -29,7 +29,7 @@ async function hideContainer(containerName: string) { async function unhideContainer(containerName: string) { try { - let data = await readData(); + const data = await readData(); const containerIndex = data.findIndex( (container: any) => container.name === containerName, ); @@ -49,7 +49,7 @@ async function unhideContainer(containerName: string) { // Tag containers async function addTagToContainer(containerName: string, tag: string) { try { - let data = await readData(); + const data = await readData(); const containerIndex = data.findIndex( (container: any) => container.name === containerName, ); @@ -72,7 +72,7 @@ async function addTagToContainer(containerName: string, tag: string) { async function removeTagFromContainer(containerName: string, tag: string) { try { - let data = await readData(); + const data = await readData(); const containerIndex = data.findIndex( (container: any) => container.name === containerName, ); @@ -94,7 +94,7 @@ async function removeTagFromContainer(containerName: string, tag: string) { // Pin containers async function pinContainer(containerName: string) { try { - let data: any = await readData(); + const data: any = await readData(); const containerIndex: number = data.findIndex( (container: any) => container.name === containerName, ); @@ -114,7 +114,7 @@ async function pinContainer(containerName: string) { async function unpinContainer(containerName: string) { try { - let data = await readData(); + const data = await readData(); const containerIndex = data.findIndex( (container: any) => container.name === containerName, ); @@ -135,7 +135,7 @@ async function unpinContainer(containerName: string) { async function setLink(containerName: string, link: string) { if (link.match(regex)) { try { - let data: any = await readData(); + const data: any = await readData(); const containerIndex: any = data.findIndex( (container: any) => container.name === containerName, ); @@ -159,7 +159,7 @@ async function setLink(containerName: string, link: string) { async function removeLink(containerName: string) { try { - let data = await readData(); + const data = await readData(); const containerIndex = data.findIndex( (container: any) => container.name === containerName, ); @@ -179,28 +179,26 @@ async function removeLink(containerName: string) { // Add/remove icon from containers async function setIcon(containerName: string, icon: string, custom: boolean) { try { - let data = await readData(); + const data = await readData(); const containerIndex: number = data.findIndex( (container: any) => container.name === containerName, ); if (custom === true) { - if (containerIndex !== -1) { - data[containerIndex].icon = `custom/${icon}`; - await saveData(data); - } else { - data.push({ name: containerName, icon: `custom/${icon}` }); - await saveData(data); - } - } - else if (containerIndex !== -1) { - data[containerIndex].icon = `${icon}`; - await saveData(data); - } - else { - data.push({ name: containerName, icon: `${icon}` }); - await saveData(data); - } + if (containerIndex !== -1) { + data[containerIndex].icon = `custom/${icon}`; + await saveData(data); + } else { + data.push({ name: containerName, icon: `custom/${icon}` }); + await saveData(data); + } + } else if (containerIndex !== -1) { + data[containerIndex].icon = `${icon}`; + await saveData(data); + } else { + data.push({ name: containerName, icon: `${icon}` }); + await saveData(data); + } } catch (error: any) { logger.error(error); throw new Error(error); @@ -209,7 +207,7 @@ async function setIcon(containerName: string, icon: string, custom: boolean) { async function removeIcon(containerName: string) { try { - let data = await readData(); + const data = await readData(); const containerIndex = data.findIndex( (container: any) => container.name === containerName, ); diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index 919148e4..dd16bf6c 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -124,7 +124,7 @@ async function prepareFilesForSync(): Promise> { } async function checkApiReachable(node: string): Promise { - let nodeUrl = + const nodeUrl = useUnsafeConnection === true ? `http://${node}/api/status` : `https://${node}/api/status`; @@ -170,7 +170,7 @@ async function synchronizeFilesWithNodes(): Promise { continue; // Skip synchronization if the node is unreachable } - let nodeUrl = + const nodeUrl = useUnsafeConnection == true ? `http://${node}/ha/sync` : `https://${node}/ha/sync`; diff --git a/src/routes/getter/routes.ts b/src/routes/getter/routes.ts index b6c89c12..8e3c6955 100644 --- a/src/routes/getter/routes.ts +++ b/src/routes/getter/routes.ts @@ -332,7 +332,7 @@ router.get("/current-schedule", (req: Request, res: Response) => { router.get("/status", async (req: Request, res: Response) => { logger.debug("Fetching /api/status"); try { - let jsonData = await checkReachability(); + const jsonData = await checkReachability(); res.status(200).json(jsonData); } catch (error: any) { logger.error(`Error while fetching data: ${error}`); diff --git a/src/utils/connectionChecker.ts b/src/utils/connectionChecker.ts index c61ffebd..289b9b37 100644 --- a/src/utils/connectionChecker.ts +++ b/src/utils/connectionChecker.ts @@ -67,7 +67,6 @@ async function checkReachability(): Promise { const parsedData = JSON.parse(data); const hosts: Host[] = parsedData.hosts; return await checkHostStatus(hosts); - } catch (error: any) { logger.error(`Error reading file: ${error}`); return undefined; diff --git a/src/utils/containerService.ts b/src/utils/containerService.ts index afc035a1..0cd09e39 100644 --- a/src/utils/containerService.ts +++ b/src/utils/containerService.ts @@ -31,8 +31,14 @@ interface AllContainerData { function loadConfig() { try { if (!fs.existsSync(configPath)) { - logger.warn(`Config file not found. Creating an empty file at ${configPath}`); - fs.writeFileSync(configPath, JSON.stringify({ "hosts": [] }, null, 2), "utf-8"); + logger.warn( + `Config file not found. Creating an empty file at ${configPath}`, + ); + fs.writeFileSync( + configPath, + JSON.stringify({ hosts: [] }, null, 2), + "utf-8", + ); } const configData = fs.readFileSync(configPath, "utf-8"); @@ -80,7 +86,7 @@ async function fetchAllContainers(): Promise { const cpuUsage = systemCpuDelta > 0 ? (cpuDelta / systemCpuDelta) * - containerStats.cpu_stats.online_cpus + containerStats.cpu_stats.online_cpus : 0; return { diff --git a/src/utils/extractHostData.ts b/src/utils/extractHostData.ts index 65577cce..25ea0168 100644 --- a/src/utils/extractHostData.ts +++ b/src/utils/extractHostData.ts @@ -49,14 +49,20 @@ function extractRelevantData(jsonData: JsonData) { return {}; } - return jsonData.version.Components.reduce((acc, component) => { - if (typeof component?.Name === 'string' && typeof component?.Version === 'string') { - acc[component.Name] = component.Version; - } - return acc; - }, {}); + return jsonData.version.Components.reduce( + (acc, component) => { + if ( + typeof component?.Name === "string" && + typeof component?.Version === "string" + ) { + acc[component.Name] = component.Version; + } + return acc; + }, + {}, + ); } catch (error) { - console.error('Error processing Components data:', error); + console.error("Error processing Components data:", error); return {}; } })(), From 0dd818e3d210d7ffd4fcacf14c3e8f0477f23882 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 19:55:37 +0100 Subject: [PATCH 047/369] Fix: Update Licensed.yml --- .github/workflows/Licensed.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Licensed.yml b/.github/workflows/Licensed.yml index e6475a78..192ec8f0 100644 --- a/.github/workflows/Licensed.yml +++ b/.github/workflows/Licensed.yml @@ -1,8 +1,9 @@ name: Licensed -on: - workflow_call: - +on: + push: + branches: '**' + jobs: validate-cached-dependency-records: runs-on: ubuntu-latest From d2d65c627b8bb6344052432792d496b0b4db5b0d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:02:40 +0100 Subject: [PATCH 048/369] Fix: Adjusted workflows --- .github/workflows/Licensed.yml | 23 +++++++++++++----- .github/workflows/anchore.yml | 12 ---------- .github/workflows/cloc.yaml | 24 +++++++++---------- .github/workflows/test-build.yaml | 5 +--- .github/workflows/validation.yml | 39 ++++--------------------------- 5 files changed, 35 insertions(+), 68 deletions(-) diff --git a/.github/workflows/Licensed.yml b/.github/workflows/Licensed.yml index e6475a78..b81989fb 100644 --- a/.github/workflows/Licensed.yml +++ b/.github/workflows/Licensed.yml @@ -1,12 +1,23 @@ name: Licensed -on: - workflow_call: +on: [push] jobs: - validate-cached-dependency-records: + license-check: runs-on: ubuntu-latest - name: Check licenses steps: - - name: Licensed CI - uses: github/licensed-ci@v1.11.1 + - name: Checkout latest code + uses: actions/checkout@v4 + - name: Use Node.js 20.x + uses: actions/setup-node@latest + with: + node-version: 20.x + - name: Run npm install + run: npm install + - name: Check licenses + uses: tangro/actions-license-check@v1.0.14 + with: + allowed-licenses: "MIT; ISC; Apache-2.0; Custom: https://www.telerik.com/kendo-angular-ui/; Custom: https://www.telerik.com/kendo-react-ui/; BSD" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_CONTEXT: ${{ toJson(github) }} diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index bb5df127..23c78abc 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -1,21 +1,9 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow checks out code, builds an image, performs a container image -# vulnerability scan with Anchore's Grype tool, and integrates the results with GitHub Advanced Security -# code scanning feature. For more information on the Anchore scan action usage -# and parameters, see https://github.com/anchore/scan-action. For more -# information on Anchore's container image scanning tool Grype, see -# https://github.com/anchore/grype name: Anchore Grype vulnerability scan on: push: branches: ["main"] pull_request: - # The branches below must be a subset of the branches above branches: ["main", "dev"] schedule: - cron: "30 9 * * 1" diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml index 9ce7e271..795ad75d 100644 --- a/.github/workflows/cloc.yaml +++ b/.github/workflows/cloc.yaml @@ -6,23 +6,23 @@ permissions: on: push: - branches: [ main ] + branches: [main, dev] pull_request: - branches: [ main, dev ] + branches: [main, dev] jobs: cloc: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3 - - name: Count Lines of Code (cloc) - uses: djdefi/cloc-action@6 - with: - options: --md --report-file=cloc.md --exclude-dir=node_modules --exclude-lang=YAML,JSON --exclude-list-file=package-lock.json - - - name: Create comment from markdown file - uses: GrantBirki/comment@v2.1.0 - with: - file: cloc.md \ No newline at end of file + - name: Count Lines of Code (cloc) + uses: djdefi/cloc-action@6 + with: + options: --md --report-file=cloc.md --exclude-dir=node_modules --exclude-lang=YAML,JSON --exclude-list-file=package-lock.json + + - name: Create comment from markdown file + uses: GrantBirki/comment@v2.1.0 + with: + file: cloc.md diff --git a/.github/workflows/test-build.yaml b/.github/workflows/test-build.yaml index 8c805d46..0b304ec2 100644 --- a/.github/workflows/test-build.yaml +++ b/.github/workflows/test-build.yaml @@ -1,9 +1,6 @@ name: Test building -on: - pull_request: - branches: - - "dev" +on: [push] permissions: packages: write diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 7dcf706a..c5150a15 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -1,45 +1,17 @@ -name: Basic validation - -on: - workflow_call: - inputs: - operating-systems: - description: "Optional input to set a list of operating systems which the workflow uses. Defaults to ['ubuntu-latest', 'windows-latest', 'macos-latest'] if not set" - required: false - type: string - default: "['ubuntu-latest', 'windows-latest', 'macos-latest']" - enable-audit: - description: "Optional input to enable npm package audit process" - required: false - type: boolean - default: true - node-version: - description: "Optional input to set the version of Node.js used to build the project. The input syntax corresponds to the setup-node's one" - required: false - type: string - default: "20.x" - node-caching: - description: "Optional input to set up caching for the setup-node action. The input syntax corresponds to the setup-node's one. Set to an empty string if caching isn't needed" - required: false - type: string - default: "npm" +on: [push] jobs: build: - runs-on: ${{matrix.operating-systems}} - strategy: - fail-fast: false - matrix: - operating-systems: ${{fromJson(inputs.operating-systems)}} + runs-on: ubuntu steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup Node.js ${{inputs.node-version}} + - name: Setup Node.js 20 uses: actions/setup-node@v4 with: - node-version: ${{inputs.node-version}} - cache: ${{inputs.node-caching}} + node-version: 20 + cache: npm - name: Install dependencies run: npm ci --ignore-scripts @@ -55,4 +27,3 @@ jobs: - name: Audit packages run: npm audit --audit-level=high - if: ${{inputs.enable-audit}} From 575d8e594173c7a04b6828fd759a9e9640b8aeae Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:32:28 +0100 Subject: [PATCH 049/369] Fix: Adjusted workflows; fixed versions --- .github/workflows/anchore.yml | 32 ++++------------- .github/workflows/build-dev.yaml | 10 +++--- .github/workflows/build-image.yml | 12 +++---- .github/workflows/build-test.yaml | 56 ++++++++++++++++++++++++++++++ .github/workflows/cloc.yaml | 6 ++-- .github/workflows/codeql.yml | 6 ++-- .github/workflows/licensed.yml | 23 ++++++++++++ .github/workflows/remove-stale.yml | 2 +- .github/workflows/validation.yml | 2 +- 9 files changed, 105 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/build-test.yaml create mode 100644 .github/workflows/licensed.yml diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 715be200..bafb5cc6 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -1,36 +1,18 @@ name: Anchore Grype vulnerability scan -on: - push: - branches: ["main"] - pull_request: - branches: ["main", "dev"] - schedule: - - cron: "30 9 * * 1" - -permissions: - contents: read - +on: [push] jobs: - Anchore-Build-Scan: - permissions: - contents: read - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + build: runs-on: ubuntu-latest steps: - - name: Check out the code - uses: actions/checkout@4 - - name: Build the Docker image + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Build the Container image run: docker build . --file Dockerfile --tag localbuild/testimage:latest - - name: Run the Anchore Grype scan action - uses: anchore/scan-action@d5aa5b6cb9414b0c7771438046ff5bcfa2854ed7 + - uses: anchore/scan-action@v3 id: scan with: image: "localbuild/testimage:latest" - fail-build: true - severity-cutoff: critical - - name: Upload vulnerability report - uses: github/codeql-action/upload-sarif + - name: upload Anchore scan SARIF report + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ${{ steps.scan.outputs.sarif }} diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index f08ba20b..b81287c2 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -14,20 +14,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up QEMU - uses: docker/setup-qemu-action + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action + uses: docker/setup-buildx-action@v3 - name: Login to Github Container Registry - uses: docker/login-action + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ github.token }} - name: Generate Docker tags - uses: docker/metadata-action + uses: docker/metadata-action@v5 id: metadata with: images: ghcr.io/${{ github.repository }} @@ -37,7 +37,7 @@ jobs: latest=false - name: Build and push - uses: docker/build-push-action + uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64, push: true diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 8c58d700..d7d131e3 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -1,4 +1,4 @@ -name: Buiod dockstatapi:latest +name: Build dockstatapi:latest on: release: @@ -13,20 +13,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up QEMU - uses: docker/setup-qemu-action + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action + uses: docker/setup-buildx-action@v3 - name: Login to Github Container Registry - uses: docker/login-action + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ github.token }} - name: Generate Docker tags - uses: docker/metadata-action + uses: docker/metadata-action@v5 id: metadata with: images: ghcr.io/${{ github.repository }} @@ -36,7 +36,7 @@ jobs: latest=true - name: Build and push - uses: docker/build-push-action + uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64 push: true diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml new file mode 100644 index 00000000..8b0af360 --- /dev/null +++ b/.github/workflows/build-test.yaml @@ -0,0 +1,56 @@ +name: Build test docker image + +on: [push] + +permissions: + packages: write + contents: read + +jobs: + build-main: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@4 + + # Set up Node.js using nvm + - name: Set up Node.js version from .nvmrc + run: | + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + nvm install + nvm use + node -v + npm -v + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate Docker tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,enable=true,priority=200,prefix=,suffix=,value=${{ github.sha }} + + - name: Build and Push Docker Images + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + push: false + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml index 4c887638..052ba123 100644 --- a/.github/workflows/cloc.yaml +++ b/.github/workflows/cloc.yaml @@ -15,14 +15,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@4 + - uses: actions/checkout@v4 - name: Count Lines of Code (cloc) - uses: djdefi/cloc-action + uses: djdefi/cloc-action@6 with: options: --md --report-file=cloc.md --exclude-dir=node_modules --exclude-lang=YAML,JSON --exclude-list-file=package-lock.json - name: Create comment from markdown file - uses: GrantBirki/comment + uses: GrantBirki/comment@v2 with: file: cloc.md diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3a14993c..b1af0a79 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,10 +27,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@4 + uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -47,6 +47,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/licensed.yml b/.github/workflows/licensed.yml new file mode 100644 index 00000000..f384e7ae --- /dev/null +++ b/.github/workflows/licensed.yml @@ -0,0 +1,23 @@ +name: License checker + +on: [push] + +jobs: + license-check: + runs-on: ubuntu-latest + steps: + - name: Checkout latest code + uses: actions/checkout@4 + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Run npm install + run: npm install + - name: Check licenses + uses: tangro/actions-license-check@v1 + with: + allowed-licenses: "MIT; ISC; Apache-2.0; Custom: https://www.telerik.com/kendo-angular-ui/; Custom: https://www.telerik.com/kendo-react-ui/; BSD" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_CONTEXT: ${{ toJson(github) }} diff --git a/.github/workflows/remove-stale.yml b/.github/workflows/remove-stale.yml index 2285ecb6..ccccef97 100644 --- a/.github/workflows/remove-stale.yml +++ b/.github/workflows/remove-stale.yml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale + - uses: actions/stale@v9 with: stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." stale-pr-message: "This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days." diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 7402102c..20bc3ef6 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -8,7 +8,7 @@ jobs: uses: actions/checkout@4 - name: Setup Node.js 20 - uses: actions/setup-node + uses: actions/setup-node@v4 with: node-version: 20 cache: npm From 491171d78df3f297537dfae3c6ecb4bbc5b5fd70 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:42:56 +0100 Subject: [PATCH 050/369] Fix: Correct workflow versions --- .github/workflows/anchore.yml | 1 + .github/workflows/build-test.yaml | 2 +- .github/workflows/cloc.yaml | 1 + .github/workflows/license.yml | 23 +++++++++++++++++++++++ .github/workflows/validation.yml | 6 ++++-- 5 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/license.yml diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index bafb5cc6..22632af8 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -12,6 +12,7 @@ jobs: id: scan with: image: "localbuild/testimage:latest" + fail-build: false - name: upload Anchore scan SARIF report uses: github/codeql-action/upload-sarif@v3 with: diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 8b0af360..f25efed2 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@4 + uses: actions/checkout@v4 # Set up Node.js using nvm - name: Set up Node.js version from .nvmrc diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml index 052ba123..9ea30547 100644 --- a/.github/workflows/cloc.yaml +++ b/.github/workflows/cloc.yaml @@ -26,3 +26,4 @@ jobs: uses: GrantBirki/comment@v2 with: file: cloc.md + issue-number: ${{ github.event.number }} diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml new file mode 100644 index 00000000..495314c0 --- /dev/null +++ b/.github/workflows/license.yml @@ -0,0 +1,23 @@ +name: License checker + +on: [push] + +jobs: + license-check: + runs-on: ubuntu-latest + steps: + - name: Checkout latest code + uses: actions/checkout@v4 + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Run npm install + run: npm install + - name: Check licenses + uses: tangro/actions-license-check@1 + with: + allowed-licenses: "MIT; ISC; Apache-2.0; Custom: https://www.telerik.com/kendo-angular-ui/; Custom: https://www.telerik.com/kendo-react-ui/; BSD" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_CONTEXT: ${{ toJson(github) }} diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 20bc3ef6..3183f54a 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -1,11 +1,13 @@ +name: "Run all tests" + on: [push] jobs: build: - runs-on: ubuntu + runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@4 + uses: actions/checkout@v4 - name: Setup Node.js 20 uses: actions/setup-node@v4 From f4c1c5e0876cefd5541b78e10ffb5f70e197d26d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:48:34 +0100 Subject: [PATCH 051/369] Fix: Workflow stuff... --- .github/workflows/cloc.yaml | 2 -- .github/workflows/license.yml | 2 +- .github/workflows/validation.yml | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml index 9ea30547..d29afa4a 100644 --- a/.github/workflows/cloc.yaml +++ b/.github/workflows/cloc.yaml @@ -5,8 +5,6 @@ permissions: pull-requests: write on: - push: - branches: [main, dev] pull_request: branches: [main, dev] diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml index 495314c0..3089a541 100644 --- a/.github/workflows/license.yml +++ b/.github/workflows/license.yml @@ -15,7 +15,7 @@ jobs: - name: Run npm install run: npm install - name: Check licenses - uses: tangro/actions-license-check@1 + uses: tangro/actions-license-check@v1.0.14 with: allowed-licenses: "MIT; ISC; Apache-2.0; Custom: https://www.telerik.com/kendo-angular-ui/; Custom: https://www.telerik.com/kendo-react-ui/; BSD" env: diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 3183f54a..7b24cf87 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -19,7 +19,7 @@ jobs: run: npm ci --ignore-scripts - name: Run prettier - run: npm run pettier + run: npm run prettier - name: Run linter run: npm run lint From 7ab3ef25210004515842a99e662e3069cc22b261 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:55:12 +0100 Subject: [PATCH 052/369] Fix: Dropping licence check (will be back) Fix: added sarif file for anchore --- .github/workflows/anchore.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 22632af8..57a7fb4a 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -13,7 +13,8 @@ jobs: with: image: "localbuild/testimage:latest" fail-build: false + output-file: ./result.sarif - name: upload Anchore scan SARIF report uses: github/codeql-action/upload-sarif@v3 with: - sarif_file: ${{ steps.scan.outputs.sarif }} + sarif_file: ./result.sarif From d53e0faab41ff40a428b6b097111f9762d33be43 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:57:10 +0100 Subject: [PATCH 053/369] Fix: Delete .github/workflows/Licensed.yml Files were deleted on my laptop but not on GH --- .github/workflows/Licensed.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .github/workflows/Licensed.yml diff --git a/.github/workflows/Licensed.yml b/.github/workflows/Licensed.yml deleted file mode 100644 index 557e7bb0..00000000 --- a/.github/workflows/Licensed.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Licensed - -on: [push] - -jobs: - license-check: - runs-on: ubuntu-latest - steps: - - name: Checkout latest code - uses: actions/checkout@4 - - name: Use Node.js 20.x - uses: actions/setup-node - with: - node-version: 20.x - - name: Run npm install - run: npm install - - name: Check licenses - uses: tangro/actions-license-check - with: - allowed-licenses: "MIT; ISC; Apache-2.0; Custom: https://www.telerik.com/kendo-angular-ui/; Custom: https://www.telerik.com/kendo-react-ui/; BSD" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_CONTEXT: ${{ toJson(github) }} From aa38d1b38d9876d56e2dfd0a7a3152b6b4151d43 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:57:32 +0100 Subject: [PATCH 054/369] Fix: Delete .github/workflows/license.yml Files were deleted on my laptop but not on GH --- .github/workflows/license.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .github/workflows/license.yml diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml deleted file mode 100644 index 3089a541..00000000 --- a/.github/workflows/license.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: License checker - -on: [push] - -jobs: - license-check: - runs-on: ubuntu-latest - steps: - - name: Checkout latest code - uses: actions/checkout@v4 - - name: Use Node.js 20.x - uses: actions/setup-node@v4 - with: - node-version: 20.x - - name: Run npm install - run: npm install - - name: Check licenses - uses: tangro/actions-license-check@v1.0.14 - with: - allowed-licenses: "MIT; ISC; Apache-2.0; Custom: https://www.telerik.com/kendo-angular-ui/; Custom: https://www.telerik.com/kendo-react-ui/; BSD" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_CONTEXT: ${{ toJson(github) }} From 66f6c60270ab15e9c0604fe0a649b0c456f6273e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:57:56 +0100 Subject: [PATCH 055/369] Fix: Renamed .github/workflows/test-build.yaml --- .github/workflows/test-build.yaml | 56 ------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 .github/workflows/test-build.yaml diff --git a/.github/workflows/test-build.yaml b/.github/workflows/test-build.yaml deleted file mode 100644 index d298b76b..00000000 --- a/.github/workflows/test-build.yaml +++ /dev/null @@ -1,56 +0,0 @@ -name: Test building - -on: [push] - -permissions: - packages: write - contents: read - -jobs: - build-main: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@4 - - # Set up Node.js using nvm - - name: Set up Node.js version from .nvmrc - run: | - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - nvm install - nvm use - node -v - npm -v - - - name: Set up QEMU - uses: docker/setup-qemu-action - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action - - - name: Login to Github Container Registry - uses: docker/login-action - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate Docker tags - uses: docker/metadata-action - id: metadata - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,enable=true,priority=200,prefix=,suffix=,value=${{ github.sha }} - - - name: Build and Push Docker Images - uses: docker/build-push-action - with: - platforms: linux/amd64,linux/arm64 - push: false - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max From 93c392c852d1298d55595654396830de059006ed Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 22:58:17 +0100 Subject: [PATCH 056/369] Fix: Delete .github/workflows/licensed.yml Files were deleted on my laptop but not on GH --- .github/workflows/licensed.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .github/workflows/licensed.yml diff --git a/.github/workflows/licensed.yml b/.github/workflows/licensed.yml deleted file mode 100644 index f384e7ae..00000000 --- a/.github/workflows/licensed.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: License checker - -on: [push] - -jobs: - license-check: - runs-on: ubuntu-latest - steps: - - name: Checkout latest code - uses: actions/checkout@4 - - name: Use Node.js 20.x - uses: actions/setup-node@v4 - with: - node-version: 20.x - - name: Run npm install - run: npm install - - name: Check licenses - uses: tangro/actions-license-check@v1 - with: - allowed-licenses: "MIT; ISC; Apache-2.0; Custom: https://www.telerik.com/kendo-angular-ui/; Custom: https://www.telerik.com/kendo-react-ui/; BSD" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_CONTEXT: ${{ toJson(github) }} From 7be77f78556398f10680ea8267ea2a51fc137786 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 26 Dec 2024 23:07:46 +0100 Subject: [PATCH 057/369] Chore: Cleaning up (gn) --- .github/workflows/anchore.yml | 5 ++--- .github/workflows/build-dev.yaml | 2 +- .github/workflows/build-image.yml | 2 +- .github/workflows/build-test.yaml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/remove-stale.yml | 2 +- .github/workflows/validation.yml | 2 +- README.md | 3 ++- TODO.md | 20 ++++++++++---------- package.json | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 57a7fb4a..84eac32e 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -1,6 +1,7 @@ name: Anchore Grype vulnerability scan on: [push] + jobs: build: runs-on: ubuntu-latest @@ -12,9 +13,7 @@ jobs: id: scan with: image: "localbuild/testimage:latest" - fail-build: false - output-file: ./result.sarif - name: upload Anchore scan SARIF report uses: github/codeql-action/upload-sarif@v3 with: - sarif_file: ./result.sarif + sarif_file: ${{ steps.scan.outputs.sarif }} diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index b81287c2..f21ab4ac 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -10,7 +10,7 @@ permissions: contents: read jobs: - build-main: + build-dev: runs-on: ubuntu-latest steps: - name: Set up QEMU diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index d7d131e3..17933f97 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -9,7 +9,7 @@ permissions: contents: read jobs: - build-main: + build-release: runs-on: ubuntu-latest steps: - name: Set up QEMU diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index f25efed2..2f2322f5 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -7,7 +7,7 @@ permissions: contents: read jobs: - build-main: + build-test: runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b1af0a79..081205c6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,7 +9,7 @@ on: - cron: "32 1 * * 5" jobs: - analyze: + codeql: name: Analyze TypeScript runs-on: "ubuntu-latest" permissions: diff --git a/.github/workflows/remove-stale.yml b/.github/workflows/remove-stale.yml index ccccef97..93d1acdc 100644 --- a/.github/workflows/remove-stale.yml +++ b/.github/workflows/remove-stale.yml @@ -4,7 +4,7 @@ on: - cron: "30 1 * * *" jobs: - stale: + remove-stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v9 diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 7b24cf87..d46610bc 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -3,7 +3,7 @@ name: "Run all tests" on: [push] jobs: - build: + validation: runs-on: ubuntu-latest steps: - name: Checkout diff --git a/README.md b/README.md index f602f3b2..50246a18 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # DockStatAPI v2 + ![Dockstat Logo](.github/DockStat.png) -*Pipelines:*
+_Pipelines:_
[![Docker Image CI](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml/badge.svg?branch=main)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml)
[![Build dockstatapi:nightly](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml)
diff --git a/TODO.md b/TODO.md index c4687d72..36d32653 100644 --- a/TODO.md +++ b/TODO.md @@ -1,13 +1,13 @@ -- [X] ~Better Offline mode using "faker" library or self written (probably self written)~ Not needed since there is a docker-compsoe file for local testing integrated inside the repo -- [X] HA compatibility -- [X] !!! Needs testing !!! Add automatic notifications when container state changes, according to selected level for notification service +- [x] ~Better Offline mode using "faker" library or self written (probably self written)~ Not needed since there is a docker-compsoe file for local testing integrated inside the repo +- [x] HA compatibility +- [x] !!! Needs testing !!! Add automatic notifications when container state changes, according to selected level for notification service - [ ] Image update and update notifications - [ ] trigger container restart / stop / start via backend routes -- [X] Add more logging -- [X] Structure code differently -- [X] Write new README and make the docs better -- [X] Update more files to correct TS syntax => remove "any" +- [x] Add more logging +- [x] Structure code differently +- [x] Write new README and make the docs better +- [x] Update more files to correct TS syntax => remove "any" - [ ] Websockets -- [X] Better /api/status endpoint with connection status of each host -- [X] Update notification service -- [X] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) +- [x] Better /api/status endpoint with connection status of each host +- [x] Update notification service +- [x] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) diff --git a/package.json b/package.json index 20af78a8..fa0e693f 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "docker:full": "docker compose up -d && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", "docker:build": "docker build . -t \"dockstatapi:local\" -f ./Dockerfile-dev && docker compose up -d", "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", - "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write", + "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write && npx prettier -c ./.github/workflows/*.{yaml,yml} --parser yaml --write && npx prettier -c ./**/*.md --parser markdown --write && npx prettier -c ./**/*.json --parser json --write", "lint": "npx eslint", "lint:fix": "npx eslint --fix" }, From 7c669ea2d369b523f13079b3b21532e12a5806cc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:12:02 +0100 Subject: [PATCH 058/369] Fix: Test new grype workflow --- .github/workflows/anchore.yml | 10 +- Dockerfile | 4 +- Dockerfile-dev | 4 +- package-lock.json | 771 +++++++++++++++++++--------------- 4 files changed, 452 insertions(+), 337 deletions(-) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 84eac32e..4c8ad741 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -6,14 +6,14 @@ jobs: build: runs-on: ubuntu-latest steps: + - name: Download Grype + run: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $GITHUB_PATH - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Build the Container image run: docker build . --file Dockerfile --tag localbuild/testimage:latest - - uses: anchore/scan-action@v3 - id: scan - with: - image: "localbuild/testimage:latest" + - name: Run Grype test + run: grype -o sarif localbuild/testimage:latest > results.sarif - name: upload Anchore scan SARIF report uses: github/codeql-action/upload-sarif@v3 with: - sarif_file: ${{ steps.scan.outputs.sarif }} + sarif_file: ./results.sarif diff --git a/Dockerfile b/Dockerfile index 26f492b5..dc4f58cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ RUN apk update && \ apk add bash -COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ RUN npm install COPY ./src ./src @@ -38,7 +38,7 @@ WORKDIR /build RUN mkdir -p /build/src/data -COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ RUN npm install --omit=dev COPY --from=builder /build/dist/* /build/src diff --git a/Dockerfile-dev b/Dockerfile-dev index 6e9452a0..bd246884 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -19,7 +19,7 @@ RUN apk update && \ apk add bash -COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ RUN npm install COPY ./src ./src @@ -38,7 +38,7 @@ WORKDIR /build RUN mkdir -p /build/src/data -COPY tsconfig.json environment.d.ts package*.json tsconfig.json yarn.lock ./ +COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ RUN npm install --omit=dev COPY --from=builder /build/dist/* /build/src diff --git a/package-lock.json b/package-lock.json index 118aa2bc..847bd243 100644 --- a/package-lock.json +++ b/package-lock.json @@ -590,6 +590,30 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/core": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", @@ -644,6 +668,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -657,16 +692,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -674,17 +699,17 @@ "dev": true, "license": "MIT" }, - "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "*" } }, "node_modules/@eslint/js": { @@ -932,13 +957,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz", - "integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", + "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.49.0" + "playwright": "1.49.1" }, "bin": { "playwright": "cli.js" @@ -1117,9 +1142,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1195,9 +1220,9 @@ } }, "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.67", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", - "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", + "version": "18.19.68", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.68.tgz", + "integrity": "sha512-QGtpFH1vB99ZmTa63K4/FU8twThj4fuVSBkGddTp7uIL/cuoLWIUSL2RcOaigBhfR+hg5pgGkBnkoOxrTVBMKw==", "dev": true, "license": "MIT", "dependencies": { @@ -1272,16 +1297,6 @@ "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/@typescript-eslint/parser": { "version": "8.18.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.2.tgz", @@ -1390,32 +1405,6 @@ "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/utils": { "version": "8.18.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.2.tgz", @@ -1627,26 +1616,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/ansi-styles/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": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ansi-styles/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/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1857,13 +1826,13 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -1951,35 +1920,33 @@ "node": ">= 10" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/call-me-maybe": { @@ -1999,22 +1966,26 @@ } }, "node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -2085,18 +2056,22 @@ } }, "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "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": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/color-string": { @@ -2118,6 +2093,21 @@ "color-support": "bin.js" } }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, "node_modules/colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", @@ -2305,23 +2295,6 @@ "dev": true, "license": "MIT" }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2348,9 +2321,9 @@ } }, "node_modules/dependency-cruiser": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.7.0.tgz", - "integrity": "sha512-522LLjHINl9r0RIZ8/6s6TqIHTuEJG3XDU2WPSm9dG0rvLUYVyQwE9ID31tDFs4OOyEhdOPaqAaAG1jRv/Zwbg==", + "version": "16.8.0", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.8.0.tgz", + "integrity": "sha512-VyBzIrLHfG7rT36URln+CTy8VSjrLB7YDlMx5vtBSHRHCOXgLUCcP4n5ZoD+s166T0i5LN33q1CvBkEOGsDTSg==", "dev": true, "license": "MIT", "dependencies": { @@ -2375,7 +2348,7 @@ "semver": "^7.6.3", "teamcity-service-messages": "^0.1.14", "tsconfig-paths-webpack-plugin": "^4.2.0", - "watskeburt": "^4.1.1" + "watskeburt": "^4.2.2" }, "bin": { "depcruise": "bin/dependency-cruise.mjs", @@ -2389,6 +2362,16 @@ "node": "^18.17||>=20" } }, + "node_modules/dependency-cruiser/node_modules/ignore": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", + "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -2460,12 +2443,12 @@ } }, "node_modules/dunder-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", - "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", + "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" }, @@ -2533,9 +2516,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2581,6 +2564,18 @@ "node": ">= 0.4" } }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", @@ -2747,21 +2742,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { @@ -2777,39 +2766,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/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/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2817,17 +2773,17 @@ "dev": true, "license": "MIT" }, - "node_modules/eslint/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==", + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "has-flag": "^4.0.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=8" + "node": "*" } }, "node_modules/espree": { @@ -2971,9 +2927,9 @@ } }, "node_modules/express-rate-limit": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz", - "integrity": "sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", "license": "MIT", "engines": { "node": ">= 16" @@ -2982,7 +2938,7 @@ "url": "https://github.com/sponsors/express-rate-limit" }, "peerDependencies": { - "express": "4 || 5 || ^5.0.0-beta.1" + "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "node_modules/express/node_modules/debug": { @@ -3024,6 +2980,19 @@ "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/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", @@ -3321,19 +3290,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz", - "integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", + "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", + "call-bind-apply-helpers": "^1.0.1", "dunder-proto": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "gopd": "^1.2.0", "has-symbols": "^1.1.0", - "hasown": "^2.0.2" + "hasown": "^2.0.2", + "math-intrinsics": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -3383,16 +3354,38 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 6" + "node": "*" } }, "node_modules/global-directory": { @@ -3451,25 +3444,13 @@ "license": "MIT" }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "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": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, "node_modules/has-symbols": { @@ -3602,9 +3583,9 @@ "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", - "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -3742,9 +3723,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { @@ -4032,6 +4013,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/log-symbols/node_modules/is-unicode-supported": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", @@ -4134,6 +4128,15 @@ "node": ">= 10" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -4273,15 +4276,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -4584,9 +4591,9 @@ } }, "node_modules/nodemon": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", - "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", + "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", "dev": true, "license": "MIT", "dependencies": { @@ -4612,6 +4619,17 @@ "url": "https://opencollective.com/nodemon" } }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/nodemon/node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -4637,6 +4655,42 @@ "fsevents": "~2.3.2" } }, + "node_modules/nodemon/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/nodemon/node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -4663,6 +4717,19 @@ "node": ">=8.10.0" } }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -4830,6 +4897,19 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "node_modules/ora/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/ora/node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", @@ -5004,13 +5084,13 @@ } }, "node_modules/playwright": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", - "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.49.0" + "playwright-core": "1.49.1" }, "bin": { "playwright": "cli.js" @@ -5023,9 +5103,9 @@ } }, "node_modules/playwright-core": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", - "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5252,6 +5332,15 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -5313,19 +5402,22 @@ } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5567,23 +5659,6 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5614,15 +5689,69 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -5902,25 +6031,29 @@ } }, "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "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": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -5956,6 +6089,16 @@ "node": ">=12.0.0" } }, + "node_modules/swagger-jsdoc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/swagger-jsdoc/node_modules/commander": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", @@ -5986,6 +6129,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/swagger-jsdoc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/swagger-parser": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", @@ -6240,46 +6395,6 @@ "node": ">=10.13.0" } }, - "node_modules/tsconfig-paths-webpack-plugin/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/tsconfig-paths-webpack-plugin/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/tsconfig-paths-webpack-plugin/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/tsx": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", From 1cc4073596dce42cd668c2c4928fa5d6dcaa815d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:13:06 +0100 Subject: [PATCH 059/369] Fix: Test new grype workflow --- .github/workflows/anchore.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 4c8ad741..00dd53c1 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download Grype - run: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $GITHUB_PATH + run: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $GITHUB_PATH/grype - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Build the Container image run: docker build . --file Dockerfile --tag localbuild/testimage:latest From a44ac5f3598d307e7442a6b1681a3ecbc4f6f4b8 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:14:59 +0100 Subject: [PATCH 060/369] Fix: Test new grype workflow --- .github/workflows/anchore.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 00dd53c1..02ad1658 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -6,14 +6,17 @@ jobs: build: runs-on: ubuntu-latest steps: + - name: Set up Grype installation path + run: echo "$HOME/bin" >> $GITHUB_PATH - name: Download Grype - run: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $GITHUB_PATH/grype - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + run: | + curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Build the Container image run: docker build . --file Dockerfile --tag localbuild/testimage:latest - name: Run Grype test run: grype -o sarif localbuild/testimage:latest > results.sarif - - name: upload Anchore scan SARIF report + - name: Upload Anchore scan SARIF report uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ./results.sarif From 9b0037899b1ff1f688fade50bdf669ab650daf44 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:17:30 +0100 Subject: [PATCH 061/369] Fix: Test new grype workflow --- .github/workflows/anchore.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 02ad1658..ba2e71a2 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -2,6 +2,10 @@ name: Anchore Grype vulnerability scan on: [push] +permissions: + contents: read + security-events: write + jobs: build: runs-on: ubuntu-latest From 02e14397d3ce5b21a78b1972e03d94a841f3d656 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:53:20 +0100 Subject: [PATCH 062/369] Feat: Added credit function (npm run license) --- .github/workflows/anchore.yml | 2 +- CREDITS.md | 52 +++++ package-lock.json | 425 ++++++++++++++++++++++++++++++++++ package.json | 4 +- src/misc/credits.sh | 28 +++ 5 files changed, 509 insertions(+), 2 deletions(-) create mode 100644 CREDITS.md create mode 100644 src/misc/credits.sh diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index ba2e71a2..2725a7cc 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -7,7 +7,7 @@ permissions: security-events: write jobs: - build: + anchore: runs-on: ubuntu-latest steps: - name: Set up Grype installation path diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 00000000..6ff66a2a --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,52 @@ +### License: (MIT AND CC-BY-3.0) + +| Name | Repository | Publisher | +|------|-------------|-----------| +| spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | + + +### License: Apache-2.0 + +| Name | Repository | Publisher | +|------|-------------|-----------| +| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | +| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | +| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | +| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | +| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | +| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | +| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | +| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | +| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | +| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | + + +### License: CC-BY-3.0 + +| Name | Repository | Publisher | +|------|-------------|-----------| +| spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | + + +### License: Python-2.0 + +| Name | Repository | Publisher | +|------|-------------|-----------| +| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | + + diff --git a/package-lock.json b/package-lock.json index 847bd243..9776ee4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "dependency-cruiser": "^16.5.0", "eslint": "^9.17.0", "globals": "^15.14.0", + "license-checker": "^25.0.1", "nodemon": "^3.1.7", "ora": "^8.1.1", "prettier": "^3.4.2", @@ -1676,12 +1677,29 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -2264,6 +2282,17 @@ } } }, + "node_modules/debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -2391,6 +2420,17 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -3483,6 +3523,13 @@ "node": ">= 0.4" } }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -3888,6 +3935,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -3955,6 +4009,153 @@ "node": ">= 0.8.0" } }, + "node_modules/license-checker": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", + "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "chalk": "^2.4.1", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "read-installed": "~4.0.3", + "semver": "^5.5.0", + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-satisfies": "^4.0.0", + "treeify": "^1.1.0" + }, + "bin": { + "license-checker": "bin/license-checker" + } + }, + "node_modules/license-checker/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/license-checker/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/license-checker/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/license-checker/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/license-checker/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/license-checker/node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/license-checker/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/license-checker/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4745,6 +4946,29 @@ "node": ">=6" } }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4755,6 +4979,13 @@ "node": ">=0.10.0" } }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true, + "license": "ISC" + }, "node_modules/npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -4951,6 +5182,38 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5341,6 +5604,49 @@ "node": ">=0.10.0" } }, + "node_modules/read-installed": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", + "integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "slide": "~1.1.3", + "util-extend": "^1.0.1" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/read-installed/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-package-json": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", + "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.1", + "json-parse-even-better-errors": "^2.3.0", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -5355,6 +5661,20 @@ "node": ">= 6" } }, + "node_modules/readdir-scoped-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, "node_modules/readdirp": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", @@ -5840,6 +6160,16 @@ "dev": true, "license": "MIT" }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "*" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -5881,6 +6211,73 @@ "node": ">= 10" } }, + "node_modules/spdx-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", + "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdx-ranges": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", + "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", + "dev": true, + "license": "(MIT AND CC-BY-3.0)" + }, + "node_modules/spdx-satisfies": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz", + "integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, "node_modules/split-ca": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", @@ -6298,6 +6695,16 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -6584,6 +6991,13 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/util-extend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", + "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", + "dev": true, + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -6600,6 +7014,17 @@ "dev": true, "license": "MIT" }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/validator": { "version": "13.12.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", diff --git a/package.json b/package.json index fa0e693f..466978d9 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write && npx prettier -c ./.github/workflows/*.{yaml,yml} --parser yaml --write && npx prettier -c ./**/*.md --parser markdown --write && npx prettier -c ./**/*.json --parser json --write", "lint": "npx eslint", - "lint:fix": "npx eslint --fix" + "lint:fix": "npx eslint --fix", + "license": "bash ./src/misc/credits.sh" }, "keywords": [], "author": "Its4Nik", @@ -60,6 +61,7 @@ "dependency-cruiser": "^16.5.0", "eslint": "^9.17.0", "globals": "^15.14.0", + "license-checker": "^25.0.1", "nodemon": "^3.1.7", "ora": "^8.1.1", "prettier": "^3.4.2", diff --git a/src/misc/credits.sh b/src/misc/credits.sh new file mode 100644 index 00000000..8d331509 --- /dev/null +++ b/src/misc/credits.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +if ! command -v jq 2>&1 >/dev/null +then + echo "ERROR: jq could not be found" + exit 1 +fi + + +LICENSE_JSON=$(npx license-checker \ + --exclude 'MIT, MIT-0, MIT OR X11, BSD, ISC, Unlicense, CC0-1.0, Python-2.0: 1' \ + --json) +{ + echo -e "# CREDITS\n" + echo "This file shows all npm packages used in DockStatAPI (also Dev packages)" +} + +jq -r ' + to_entries | + group_by(.value.licenses)[] | + "### License: \(.[0].value.licenses)\n\n" + + "| Name | Repository | Publisher |\n|------|-------------|-----------|\n" + + (map( + "| \(.key) | \(.value.repository // "N/A") | \(.value.publisher // "N/A") |" + ) | join("\n")) + "\n\n" +' <<< "$LICENSE_JSON" >> CREDITS.md + +echo "Markdown file with license information has been created: CREDITS.md" From 71a43ee2ae24991593799810a3b349c74d4b45ea Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:53:56 +0100 Subject: [PATCH 063/369] Feat: Added credit function (npm run license) --- CREDITS.md | 3 +++ src/misc/credits.sh | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CREDITS.md b/CREDITS.md index 6ff66a2a..62f87e6a 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,3 +1,6 @@ +# CREDITS + +This file shows all npm packages used in DockStatAPI (also Dev packages) ### License: (MIT AND CC-BY-3.0) | Name | Repository | Publisher | diff --git a/src/misc/credits.sh b/src/misc/credits.sh index 8d331509..8a028a74 100644 --- a/src/misc/credits.sh +++ b/src/misc/credits.sh @@ -10,10 +10,11 @@ fi LICENSE_JSON=$(npx license-checker \ --exclude 'MIT, MIT-0, MIT OR X11, BSD, ISC, Unlicense, CC0-1.0, Python-2.0: 1' \ --json) + { echo -e "# CREDITS\n" echo "This file shows all npm packages used in DockStatAPI (also Dev packages)" -} +} > CREDITS.md jq -r ' to_entries | From 2912bc85f03e9a5f688914a2e7fed14afeebb0d3 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:54:25 +0100 Subject: [PATCH 064/369] Feat: Added credit function (npm run license) --- src/misc/credits.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/misc/credits.sh b/src/misc/credits.sh index 8a028a74..3db14f64 100644 --- a/src/misc/credits.sh +++ b/src/misc/credits.sh @@ -13,7 +13,7 @@ LICENSE_JSON=$(npx license-checker \ { echo -e "# CREDITS\n" - echo "This file shows all npm packages used in DockStatAPI (also Dev packages)" + echo -e "This file shows all npm packages used in DockStatAPI (also Dev packages)\n" } > CREDITS.md jq -r ' From 887d5df8e8215119ab925e43fcd653a1e4fde845 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 19:54:44 +0100 Subject: [PATCH 065/369] Feat: Git pre-commit hook testing` --- CREDITS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CREDITS.md b/CREDITS.md index 62f87e6a..be34b479 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,6 +1,7 @@ # CREDITS This file shows all npm packages used in DockStatAPI (also Dev packages) + ### License: (MIT AND CC-BY-3.0) | Name | Repository | Publisher | From b36a03e175d5bb9939a70555a7229ac56883e570 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 21:56:02 +0100 Subject: [PATCH 066/369] Fix: Add linting Chore: Update docs (can't see it here lmao) --- CREDITS.md | 73 ++++++++++++------------- README.md | 15 +++++ package.json | 5 +- src/config/db.ts | 29 ++++------ src/config/initFiles.ts | 1 - src/misc/createEnvDev.sh | 4 ++ src/misc/createEnvFile.sh | 1 + src/routes/frontendController/routes.ts | 3 +- 8 files changed, 70 insertions(+), 61 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index be34b479..050b430b 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -4,53 +4,48 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) ### License: (MIT AND CC-BY-3.0) -| Name | Repository | Publisher | -|------|-------------|-----------| +| Name | Repository | Publisher | +| ----------------- | -------------------------------------------- | -------------------- | | spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | - ### License: Apache-2.0 -| Name | Repository | Publisher | -|------|-------------|-----------| -| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | -| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | -| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | -| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | -| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | -| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | -| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | -| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | -| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | - +| Name | Repository | Publisher | +| ------------------------------------ | ------------------------------------------------------------- | --------------------- | +| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | +| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | +| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | +| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | +| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | +| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | +| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | +| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | +| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | +| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | ### License: CC-BY-3.0 -| Name | Repository | Publisher | -|------|-------------|-----------| +| Name | Repository | Publisher | +| --------------------- | -------------------------------------------------- | -------------------- | | spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | - ### License: Python-2.0 -| Name | Repository | Publisher | -|------|-------------|-----------| -| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | - - +| Name | Repository | Publisher | +| -------------- | ---------------------------------- | --------- | +| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | diff --git a/README.md b/README.md index 50246a18..e24b1497 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,21 @@ _⚠️ = Deprecation warning_ - [⚠️ Integrations](https://outline.itsnik.de/s/dockstat/doc/integrations-Agq1oL6HxF) - [⚠️ Backend API reference](https://outline.itsnik.de/s/dockstat/doc/backend-api-reference-YzcBbDvY33) +# Dependencies + +Please see [CREDITS.md](./CREDITS.md). + +To create the credits file use: `npm run license` + +Or if you want it as a pre-commit hook create this file: + +```bash +#!/bin/bash +# .git/hooks/pre-commit + +npm run license +``` + # DockStat(APIs) goals DockStack tries to be a lightweigh and more "dashboard" like then [portainer](https://github.com/portainer/portainer), [cAdvisor](https://github.com/google/cadvisor), [dockge](https://github.com/louislam/dockge), ... diff --git a/package.json b/package.json index 466978d9..8190b94e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dev": "npm run local-env-file && nodemon", "dev:trace": "npm run local-env-file && nodemon --trace-uncaught --trace-warnings", "dep": "bash ./src/utils/createDependencyGraph.sh", - "dep:remove": "bash ./src/utils/removeUnusedDeps.sh && bash ./src/utils/createDependencyGraph.sh", + "dep:remove": "bash ./src/utils/removeUnusedDeps.sh && npm run dep", "build": "npx tsc", "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", "mini": "bash ./src/misc/minifyDist.sh", @@ -21,7 +21,8 @@ "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write && npx prettier -c ./.github/workflows/*.{yaml,yml} --parser yaml --write && npx prettier -c ./**/*.md --parser markdown --write && npx prettier -c ./**/*.json --parser json --write", "lint": "npx eslint", "lint:fix": "npx eslint --fix", - "license": "bash ./src/misc/credits.sh" + "license": "bash ./src/misc/credits.sh", + "finish": "npm run local-env-file && npm run license && npm run prettier && npm run lint" }, "keywords": [], "author": "Its4Nik", diff --git a/src/config/db.ts b/src/config/db.ts index 80861350..6e2c91c1 100644 --- a/src/config/db.ts +++ b/src/config/db.ts @@ -3,26 +3,21 @@ import logger from "../utils/logger"; const dbPath: string = "./src/data/database.db"; -const db: sqlite3.Database = new sqlite3.Database( - dbPath, - (err: Error | null) => { - if (err) { - logger.error("Error opening database:", err.message); - } else { - db.run( - `CREATE TABLE IF NOT EXISTS data ( +const db: sqlite3.Database = new sqlite3.Database(dbPath, (error: any) => { + if (error) { + logger.error("Error opening database:", error.message); + } else { + db.run( + `CREATE TABLE IF NOT EXISTS data ( id INTEGER PRIMARY KEY AUTOINCREMENT, info TEXT NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )`, - () => { - logger.info( - "Database created / opened successfully, table is ready.", - ); - }, - ); - } - }, -); + () => { + logger.info("Database created / opened successfully, table is ready."); + }, + ); + } +}); export default db; diff --git a/src/config/initFiles.ts b/src/config/initFiles.ts index 1f8776a6..79822661 100644 --- a/src/config/initFiles.ts +++ b/src/config/initFiles.ts @@ -1,6 +1,5 @@ import { writeFileSync, existsSync } from "fs"; import logger from "../utils/logger"; -import path from "path"; const files = [ { diff --git a/src/misc/createEnvDev.sh b/src/misc/createEnvDev.sh index dde36f63..4a5a0bbe 100755 --- a/src/misc/createEnvDev.sh +++ b/src/misc/createEnvDev.sh @@ -1,5 +1,9 @@ +#!/bin/bash + +# Version VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" +# Docker if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then RUNNING_IN_DOCKER="true" else diff --git a/src/misc/createEnvFile.sh b/src/misc/createEnvFile.sh index d47eaa9c..754eab5a 100644 --- a/src/misc/createEnvFile.sh +++ b/src/misc/createEnvFile.sh @@ -9,6 +9,7 @@ if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then else RUNNING_IN_DOCKER="false" fi + echo -n "\ { \"VERSION\": \"${VERSION}\", diff --git a/src/routes/frontendController/routes.ts b/src/routes/frontendController/routes.ts index fe5d8411..540444af 100644 --- a/src/routes/frontendController/routes.ts +++ b/src/routes/frontendController/routes.ts @@ -1,5 +1,4 @@ import express from "express"; -import logger from "../../utils/logger"; const router = express.Router(); import { hideContainer, @@ -69,7 +68,7 @@ router.post("/show/:containerName", async (req, res) => { try { await unhideContainer(containerName); res.status(200).json({ message: "Container unhidden successfully." }); - } catch (error: any) { + } catch (error) { res.status(500).json({ error: error.message }); } }); From c2fd998cb8e28d812259fbf61fabf807ec85bbe9 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 22:01:56 +0100 Subject: [PATCH 067/369] Fix: Fixing dep:remove logic --- CREDITS.md | 73 +++---- package-lock.json | 148 --------------- package.json | 2 - src/misc/dependencyGraphs/mermaid-all.txt | 222 ++++++++++++---------- src/utils/removeUnusedDeps.sh | 36 ++-- 5 files changed, 178 insertions(+), 303 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 050b430b..be34b479 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -4,48 +4,53 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) ### License: (MIT AND CC-BY-3.0) -| Name | Repository | Publisher | -| ----------------- | -------------------------------------------- | -------------------- | +| Name | Repository | Publisher | +|------|-------------|-----------| | spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | + ### License: Apache-2.0 -| Name | Repository | Publisher | -| ------------------------------------ | ------------------------------------------------------------- | --------------------- | -| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | -| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | -| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | -| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | -| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | -| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | -| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | -| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | -| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | +| Name | Repository | Publisher | +|------|-------------|-----------| +| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | +| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | +| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | +| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | +| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | +| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | +| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | +| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | +| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | +| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | + ### License: CC-BY-3.0 -| Name | Repository | Publisher | -| --------------------- | -------------------------------------------------- | -------------------- | +| Name | Repository | Publisher | +|------|-------------|-----------| | spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | + ### License: Python-2.0 -| Name | Repository | Publisher | -| -------------- | ---------------------------------- | --------- | -| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | +| Name | Repository | Publisher | +|------|-------------|-----------| +| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | + + diff --git a/package-lock.json b/package-lock.json index 9776ee4d..ba55c01c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ "express-rate-limit": "^7.4.1", "https": "^1.0.0", "ipaddr.js": "^2.2.0", - "node-fetch": "^3.3.2", "nodemailer": "^6.9.16", "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", @@ -33,7 +32,6 @@ "@types/express": "^5.0.0", "@types/express-handlebars": "^5.3.1", "@types/node": "^22.9.0", - "@types/node-fetch": "^2.6.12", "@types/nodemailer": "^6.4.17", "@types/supports-color": "^8.1.3", "@types/swagger-jsdoc": "^6.0.4", @@ -1152,17 +1150,6 @@ "undici-types": "~6.20.0" } }, - "node_modules/@types/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, "node_modules/@types/nodemailer": { "version": "6.4.17", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", @@ -1715,13 +1702,6 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2136,19 +2116,6 @@ "text-hex": "1.0.x" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -2256,15 +2223,6 @@ "node": ">= 8" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2324,16 +2282,6 @@ "dev": true, "license": "MIT" }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -3070,29 +3018,6 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3202,33 +3127,6 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, - "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -4667,43 +4565,6 @@ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", "license": "MIT" }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -7056,15 +6917,6 @@ "node": "^18||>=20" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 8190b94e..9acd9525 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "express-rate-limit": "^7.4.1", "https": "^1.0.0", "ipaddr.js": "^2.2.0", - "node-fetch": "^3.3.2", "nodemailer": "^6.9.16", "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", @@ -52,7 +51,6 @@ "@types/express": "^5.0.0", "@types/express-handlebars": "^5.3.1", "@types/node": "^22.9.0", - "@types/node-fetch": "^2.6.12", "@types/nodemailer": "^6.4.17", "@types/supports-color": "^8.1.3", "@types/swagger-jsdoc": "^6.0.4", diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt index 995f295d..d2bae0c3 100644 --- a/src/misc/dependencyGraphs/mermaid-all.txt +++ b/src/misc/dependencyGraphs/mermaid-all.txt @@ -3,123 +3,143 @@ flowchart LR 0["server.ts"] subgraph 1["config"] 2["hostsystem.ts"] -B["db.ts"] -1G["swaggerConfig.ts"] +4["variables.ts"] +C["initFiles.ts"] +F["db.ts"] +1L["swaggerConfig.ts"] end 3["os"] -subgraph 4["controllers"] -5["highAvailability.ts"] -9["proxy.ts"] -A["scheduler.ts"] -C["fetchData.ts"] -R["frontendConfiguration.ts"] +subgraph 5["data"] +6["variables.json"] end -6["util"] -7["init.ts"] -8["process"] -subgraph D["utils"] -E["containerService.ts"] -F["dockerClient.ts"] -U["connectionChecker.ts"] -W["extractHostData.ts"] -X["writeOfflineLog.ts"] -subgraph 12["notifications"] -13["_notify.ts"] -14["discord.ts"] -16["_template.ts"] -17["email.ts"] -18["pushbullet.ts"] -19["pushover.ts"] -1A["slack.ts"] -1B["telegram.ts"] -1C["whatsapp.ts"] +subgraph 7["controllers"] +8["highAvailability.ts"] +D["proxy.ts"] +E["scheduler.ts"] +G["fetchData.ts"] +W["frontendConfiguration.ts"] end -1F["swaggerDocs.ts"] +9["util"] +A["init.ts"] +B["process"] +subgraph H["utils"] +I["containerService.ts"] +J["dockerClient.ts"] +M["rateLimitFS.ts"] +Z["connectionChecker.ts"] +11["extractHostData.ts"] +12["writeOfflineLog.ts"] +subgraph 17["notifications"] +18["_notify.ts"] +19["discord.ts"] +1B["_template.ts"] +1C["email.ts"] +1D["pushbullet.ts"] +1E["pushover.ts"] +1F["slack.ts"] +1G["telegram.ts"] +1H["whatsapp.ts"] end -subgraph G["middleware"] -H["authMiddleware.ts"] -I["checkLock.ts"] -J["rateLimiter.ts"] +1K["swaggerDocs.ts"] end -subgraph K["routes"] -subgraph L["auth"] -M["routes.ts"] +subgraph K["middleware"] +L["authMiddleware.ts"] +N["checkLock.ts"] +O["rateLimiter.ts"] end -subgraph N["data"] -O["routes.ts"] +subgraph P["routes"] +subgraph Q["auth"] +R["routes.ts"] end -subgraph P["frontendController"] -Q["routes.ts"] -end -subgraph S["getter"] +subgraph S["data"] T["routes.ts"] end -subgraph Y["highavailability"] -Z["routes.ts"] +subgraph U["frontendController"] +V["routes.ts"] +end +subgraph X["getter"] +Y["routes.ts"] +end +subgraph 13["highavailability"] +14["routes.ts"] end -subgraph 10["notifications"] -11["routes.ts"] +subgraph 15["notifications"] +16["routes.ts"] end -subgraph 1D["setter"] -1E["routes.ts"] +subgraph 1I["setter"] +1J["routes.ts"] end end -V["net"] -15["https"] +10["net"] +1A["https"] 0-->2 -0-->5 -0-->7 +0-->8 +0-->A +2-->4 2-->3 -5-->6 -7-->9 -7-->A -7-->H -7-->I -7-->J -7-->M -7-->O -7-->Q -7-->T -7-->Z -7-->11 -7-->1E -7-->1F -7-->8 -A-->B +4-->6 +8-->4 +8-->9 A-->C -C-->B -C-->E +A-->D +A-->E +A-->L +A-->N +A-->O +A-->R +A-->T +A-->V +A-->Y +A-->14 +A-->16 +A-->1J +A-->1K +A-->B +D-->4 E-->F -O-->B -Q-->R -T-->A -T-->U -T-->E +E-->G +G-->F +G-->I +I-->J +L-->M +N-->M T-->F -T-->W -T-->X -U-->V -Z-->5 -11-->13 -13-->14 -13-->17 -13-->18 -13-->19 -13-->1A -13-->1B -13-->1C -14-->16 -14-->15 -17-->16 -18-->16 -18-->15 -19-->16 -19-->15 -1A-->16 -1A-->15 -1B-->16 -1B-->15 -1C-->16 -1C-->15 -1E-->A -1F-->1G +V-->W +Y-->E +Y-->Z +Y-->I +Y-->J +Y-->11 +Y-->12 +Z-->10 +14-->8 +16-->18 +18-->19 +18-->1C +18-->1D +18-->1E +18-->1F +18-->1G +18-->1H +19-->4 +19-->1B +19-->1A +1C-->4 +1C-->1B +1D-->4 +1D-->1B +1D-->1A +1E-->4 +1E-->1B +1E-->1A +1F-->4 +1F-->1B +1F-->1A +1G-->4 +1G-->1B +1G-->1A +1H-->4 +1H-->1B +1H-->1A +1J-->E +1K-->1L diff --git a/src/utils/removeUnusedDeps.sh b/src/utils/removeUnusedDeps.sh index df72f4b4..5e806df3 100755 --- a/src/utils/removeUnusedDeps.sh +++ b/src/utils/removeUnusedDeps.sh @@ -2,35 +2,35 @@ echo "Creating unused dependency list" -TMP="$(npx depcheck --ignores @types/node-fetch,uglify-js,@types/supports-color,ipaddr.js,dependency-cruiser,tsx,@types/bcrypt,@types/express,@types/express-handlebars,@types/node,ts-node --quiet --oneline | tail -n 1 | tr -d '\n')" +TMP="$(npx depcheck --ignores https,@typescript-eslint/eslint-plugin,@typescript-eslint/parser,license-checker,uglify-js,@types/supports-color,ipaddr.js,dependency-cruiser,tsx,@types/bcrypt,@types/express,@types/express-handlebars,@types/node,ts-node --quiet --oneline | tail -n 1 | tr -d '\n')" -lines=$(echo "$TMP" | tr -s ' ' '\n' | wc -l) +lines=$(echo -n "$TMP" | tr -s ' ' '\n' | wc -l) if ((lines == 0)); then echo "No unused dependencies." else echo - echo "Removing these unused dependencies:" + echo "Removing these unused dependencies ($lines):" for entry in $TMP; do echo "$entry" done echo -fi -read -n 1 -p "Delete unused dependencies? (y/n) " input -echo + read -n 1 -p "Delete unused dependencies? (y/n) " input + echo -case $input in - Y|y) - COMMAND=$(echo "npm remove $TMP") - $COMMAND - exit 0 - ;; - *) - echo "Aborting" - exit 1 - ;; -esac + case $input in + Y|y) + COMMAND=$(echo "npm remove $TMP") + $COMMAND + exit 0 + ;; + *) + echo "Aborting" + exit 1 + ;; + esac +fi -exit 2 +exit 0 From ba34e961300d705060959062be0b666d4b4ff86d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 22:10:10 +0100 Subject: [PATCH 068/369] Fix: Fixing some logic things and docs --- src/misc/dependencyGraphs/mermaid-all.txt | 226 ++++++++++------------ src/utils/createDependencyGraph.sh | 5 +- 2 files changed, 107 insertions(+), 124 deletions(-) diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt index d2bae0c3..e81fdf84 100644 --- a/src/misc/dependencyGraphs/mermaid-all.txt +++ b/src/misc/dependencyGraphs/mermaid-all.txt @@ -1,145 +1,127 @@ flowchart LR 0["server.ts"] -subgraph 1["config"] -2["hostsystem.ts"] -4["variables.ts"] -C["initFiles.ts"] -F["db.ts"] -1L["swaggerConfig.ts"] +subgraph 1["controllers"] +2["highAvailability.ts"] +A["proxy.ts"] +B["scheduler.ts"] +D["fetchData.ts"] +T["frontendConfiguration.ts"] end -3["os"] -subgraph 5["data"] -6["variables.json"] +3["util"] +subgraph 4["config"] +5["variables.ts"] +9["initFiles.ts"] +C["db.ts"] +1F["swaggerConfig.ts"] end -subgraph 7["controllers"] -8["highAvailability.ts"] -D["proxy.ts"] -E["scheduler.ts"] -G["fetchData.ts"] -W["frontendConfiguration.ts"] +subgraph 6["data"] +7["variables.json"] end -9["util"] -A["init.ts"] -B["process"] -subgraph H["utils"] -I["containerService.ts"] -J["dockerClient.ts"] -M["rateLimitFS.ts"] -Z["connectionChecker.ts"] -11["extractHostData.ts"] -12["writeOfflineLog.ts"] -subgraph 17["notifications"] -18["_notify.ts"] -19["discord.ts"] -1B["_template.ts"] -1C["email.ts"] -1D["pushbullet.ts"] -1E["pushover.ts"] -1F["slack.ts"] -1G["telegram.ts"] -1H["whatsapp.ts"] +8["init.ts"] +subgraph E["utils"] +F["containerService.ts"] +G["dockerClient.ts"] +J["rateLimitFS.ts"] +W["connectionChecker.ts"] +X["writeOfflineLog.ts"] +subgraph 12["notifications"] +13["_notify.ts"] +14["discord.ts"] +15["_template.ts"] +16["email.ts"] +17["pushbullet.ts"] +18["pushover.ts"] +19["slack.ts"] +1A["telegram.ts"] +1B["whatsapp.ts"] end -1K["swaggerDocs.ts"] +1E["swaggerDocs.ts"] end -subgraph K["middleware"] -L["authMiddleware.ts"] -N["checkLock.ts"] -O["rateLimiter.ts"] +subgraph H["middleware"] +I["authMiddleware.ts"] +K["checkLock.ts"] +L["rateLimiter.ts"] end -subgraph P["routes"] -subgraph Q["auth"] -R["routes.ts"] +subgraph M["routes"] +subgraph N["auth"] +O["routes.ts"] end -subgraph S["data"] -T["routes.ts"] +subgraph P["data"] +Q["routes.ts"] end -subgraph U["frontendController"] -V["routes.ts"] +subgraph R["frontendController"] +S["routes.ts"] end -subgraph X["getter"] -Y["routes.ts"] +subgraph U["getter"] +V["routes.ts"] end -subgraph 13["highavailability"] -14["routes.ts"] +subgraph Y["highavailability"] +Z["routes.ts"] end -subgraph 15["notifications"] -16["routes.ts"] +subgraph 10["notifications"] +11["routes.ts"] end -subgraph 1I["setter"] -1J["routes.ts"] +subgraph 1C["setter"] +1D["routes.ts"] end end -10["net"] -1A["https"] 0-->2 0-->8 -0-->A -2-->4 +2-->5 2-->3 -4-->6 -8-->4 +5-->7 8-->9 -A-->C -A-->D -A-->E -A-->L -A-->N -A-->O -A-->R -A-->T -A-->V -A-->Y -A-->14 -A-->16 -A-->1J -A-->1K -A-->B -D-->4 -E-->F -E-->G -G-->F -G-->I +8-->A +8-->B +8-->I +8-->K +8-->L +8-->O +8-->Q +8-->S +8-->V +8-->Z +8-->11 +8-->1D +8-->1E +A-->5 +B-->C +B-->D +D-->C +D-->F +F-->G I-->J -L-->M -N-->M -T-->F +K-->J +Q-->C +S-->T +V-->B V-->W -Y-->E -Y-->Z -Y-->I -Y-->J -Y-->11 -Y-->12 -Z-->10 -14-->8 -16-->18 -18-->19 -18-->1C -18-->1D -18-->1E -18-->1F -18-->1G -18-->1H -19-->4 -19-->1B -19-->1A -1C-->4 -1C-->1B -1D-->4 -1D-->1B -1D-->1A -1E-->4 -1E-->1B -1E-->1A -1F-->4 -1F-->1B -1F-->1A -1G-->4 -1G-->1B -1G-->1A -1H-->4 -1H-->1B -1H-->1A -1J-->E -1K-->1L +V-->F +V-->G +V-->X +Z-->2 +11-->13 +13-->14 +13-->16 +13-->17 +13-->18 +13-->19 +13-->1A +13-->1B +14-->5 +14-->15 +16-->5 +16-->15 +17-->5 +17-->15 +18-->5 +18-->15 +19-->5 +19-->15 +1A-->5 +1A-->15 +1B-->5 +1B-->15 +1D-->B +1E-->1F diff --git a/src/utils/createDependencyGraph.sh b/src/utils/createDependencyGraph.sh index c8229992..9c220f7a 100755 --- a/src/utils/createDependencyGraph.sh +++ b/src/utils/createDependencyGraph.sh @@ -1,6 +1,7 @@ #!/bin/bash cd src || exit 1 TMP=$(mktemp) +IGNORE="../node_modules|logger|.dependency-cruiser|path|fs|os|https|net|process" cat ./server.ts | grep "./routes" | awk '{print $2,$4}' > $TMP @@ -14,7 +15,7 @@ spawn_worker(){ npx depcruise \ -p cli-feedback \ -T mermaid \ - -x "../node_modules|logger|.dependency-cruiser|path|fs|net" \ + -x "$IGNORE" \ -f ./misc/dependencyGraphs/mermaid-${route}.txt \ ${target_route} || exit 1 } @@ -26,7 +27,7 @@ done < <(cat $TMP) npx depcruise \ -p cli-feedback \ -T mermaid \ - -x "../node_modules|logger|.dependency-cruiser|path|fs" \ + -x "$IGNORE" \ -f ./misc/dependencyGraphs/mermaid-all.txt \ ./server.ts || exit 1 From 9fdb89f6a3769d28f33f6b666f45f6f1aad91732 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 22:18:41 +0100 Subject: [PATCH 069/369] Fix: Fixing build (hopefully) --- src/routes/highavailability/routes.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/routes/highavailability/routes.ts b/src/routes/highavailability/routes.ts index bc4cb794..3fadb02e 100644 --- a/src/routes/highavailability/routes.ts +++ b/src/routes/highavailability/routes.ts @@ -3,9 +3,7 @@ import { Router, Request, Response } from "express"; import logger from "../../utils/logger"; import { readConfig, - synchronizeFilesWithNodes, prepareFilesForSync, - HighAvailabilityConfig, ensureFileExists, } from "../../controllers/highAvailability"; @@ -44,7 +42,7 @@ router.get("/config", async (req: Request, res: Response) => { router.post( "/sync", async ( - req: Request<{}, {}, SyncRequestBody>, + req: Request<{}, {}, SyncRequestBody>, // eslint-disable-line res: Response, ): Promise => { try { From 8e0967be7d789ed48c07f058971b6850db2044a8 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 27 Dec 2024 22:24:17 +0100 Subject: [PATCH 070/369] Fix: Fixing build (hopefully) --- src/routes/frontendController/routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/frontendController/routes.ts b/src/routes/frontendController/routes.ts index 540444af..0fce63e0 100644 --- a/src/routes/frontendController/routes.ts +++ b/src/routes/frontendController/routes.ts @@ -68,7 +68,7 @@ router.post("/show/:containerName", async (req, res) => { try { await unhideContainer(containerName); res.status(200).json({ message: "Container unhidden successfully." }); - } catch (error) { + } catch (error: any) { res.status(500).json({ error: error.message }); } }); From ea8693acb9f1b45da1dce5d8eeadce79212b0fd7 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 10:26:06 +0100 Subject: [PATCH 071/369] Fix: Linting (No more any :D) --- src/config/db.ts | 6 +- src/config/loggerConfig.ts | 1 - src/config/swaggerConfig.ts | 2 +- src/controllers/containerController.ts | 14 +- src/controllers/fetchData.ts | 4 +- src/controllers/frontendConfiguration.ts | 111 +- src/controllers/highAvailability.ts | 2 +- src/controllers/notificationController.ts | 2 + src/controllers/scheduler.ts | 6 +- src/data/frontendConfiguration.json | 12 +- src/init.ts | 2 +- src/middleware/authMiddleware.ts | 2 +- src/routes/auth/routes.ts | 33 +- src/routes/data/routes.ts | 46 +- src/routes/frontendController/routes.ts | 40 +- src/routes/getter/routes.ts | 41 +- src/routes/notifications/routes.ts | 6 +- src/routes/setter/routes.ts | 10 +- src/typings/dockerConfig.ts | 10 + src/typings/frontendConfig.ts | 12 + src/typings/states.ts | 10 + src/typings/table.ts | 7 + src/utils/connectionChecker.ts | 4 +- src/utils/containerService.ts | 18 +- src/utils/dockerClient.ts | 2 +- src/utils/notifications/_notify.ts | 5 +- src/utils/notifications/_template.ts | 29 +- src/utils/notifications/email.ts | 4 +- src/utils/swaggerDocs.ts | 4 +- src/utils/writeOfflineLog.ts | 26 - yarn.lock | 2852 --------------------- 31 files changed, 259 insertions(+), 3064 deletions(-) create mode 100644 src/typings/dockerConfig.ts create mode 100644 src/typings/frontendConfig.ts create mode 100644 src/typings/states.ts create mode 100644 src/typings/table.ts delete mode 100644 src/utils/writeOfflineLog.ts delete mode 100644 yarn.lock diff --git a/src/config/db.ts b/src/config/db.ts index 6e2c91c1..edfe3832 100644 --- a/src/config/db.ts +++ b/src/config/db.ts @@ -3,9 +3,9 @@ import logger from "../utils/logger"; const dbPath: string = "./src/data/database.db"; -const db: sqlite3.Database = new sqlite3.Database(dbPath, (error: any) => { - if (error) { - logger.error("Error opening database:", error.message); +const db: sqlite3.Database = new sqlite3.Database(dbPath, (error: unknown) => { + if (error as Error) { + logger.error("Error opening database:", (error as Error).message); } else { db.run( `CREATE TABLE IF NOT EXISTS data ( diff --git a/src/config/loggerConfig.ts b/src/config/loggerConfig.ts index 45feb5c7..5d1a33e4 100644 --- a/src/config/loggerConfig.ts +++ b/src/config/loggerConfig.ts @@ -7,7 +7,6 @@ const red = "\x1b[31m"; const green = "\x1b[32m"; const yellow = "\x1b[33m"; const blue = "\x1b[34m"; -const pink = "\x1b[38;5;213m"; // Pink color for sync logs const ignoreExitListenerLogs = format((info) => { if ( diff --git a/src/config/swaggerConfig.ts b/src/config/swaggerConfig.ts index 630805e9..cab967f8 100644 --- a/src/config/swaggerConfig.ts +++ b/src/config/swaggerConfig.ts @@ -18,7 +18,7 @@ const options: { }; }; security: Array<{ - passwordAuth: any[]; + passwordAuth: unknown[]; }>; }; apis: string[]; diff --git a/src/controllers/containerController.ts b/src/controllers/containerController.ts index 1532681e..8d3bef30 100644 --- a/src/controllers/containerController.ts +++ b/src/controllers/containerController.ts @@ -10,12 +10,12 @@ const getContainers = async (req: Request, res: Response): Promise => { const containers = await docker.listContainers(); res.status(200).json(containers); - } catch (error: any) { + } catch (error: unknown) { logger.error( - `Error fetching containers from host: ${host} - ${error.message || "Unknown error"} - Full error: ${JSON.stringify(error, null, 2)}`, + `Error fetching containers from host: ${host} - ${(error as Error).message || "Unknown error"} - Full error: ${JSON.stringify(error, null, 2)}`, ); res.status(500).json({ - error: `Error fetching containers: ${error.message || "Unknown error"}`, + error: `Error fetching containers: ${(error as Error).message || "Unknown error"}`, }); } }; @@ -36,13 +36,15 @@ const getContainerStats = async ( `Successfully fetched stats for container: ${containerID} from host: ${containerHost}`, ); res.status(200).json(stats); - } catch (error: any) { + } catch (error: unknown) { logger.error( - `Error fetching stats for container: ${containerID} from host: ${containerHost} - ${error.message}`, + `Error fetching stats for container: ${containerID} from host: ${containerHost} - ${(error as Error).message}`, ); res .status(500) - .json({ error: `Error fetching container stats: ${error.message}` }); + .json({ + error: `Error fetching container stats: ${(error as Error).message}`, + }); } }; diff --git a/src/controllers/fetchData.ts b/src/controllers/fetchData.ts index 238e8262..dfc24878 100644 --- a/src/controllers/fetchData.ts +++ b/src/controllers/fetchData.ts @@ -66,9 +66,9 @@ const fetchData = async (): Promise => { } else { logger.info("No state change detected, notifications not triggered."); } - } catch (error: any) { + } catch (error: unknown) { logger.error( - `Error fetching data: ${JSON.stringify(error)} \nStack trace: ${error.stack}`, + `Error fetching data: ${JSON.stringify(error)} \nStack trace: ${(error as Error).stack}`, ); } }; diff --git a/src/controllers/frontendConfiguration.ts b/src/controllers/frontendConfiguration.ts index 4d31943e..e8e035c1 100644 --- a/src/controllers/frontendConfiguration.ts +++ b/src/controllers/frontendConfiguration.ts @@ -4,6 +4,7 @@ const dataPath: string = "./src/data/frontendConfiguration.json"; const expression: string = "https?://(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}([-a-zA-Z0-9()@:%_+.~#?&//=]*)"; const regex = new RegExp(expression); +import { FrontendConfig } from "../typings/frontendConfig"; /////////////////////////////////////////////////////////////// // Hide Containers: @@ -11,7 +12,7 @@ async function hideContainer(containerName: string) { try { const data = await readData(); const containerIndex = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -21,9 +22,9 @@ async function hideContainer(containerName: string) { data.push({ name: containerName, hidden: true }); await saveData(data); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -31,7 +32,7 @@ async function unhideContainer(containerName: string) { try { const data = await readData(); const containerIndex = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -39,9 +40,9 @@ async function unhideContainer(containerName: string) { await saveData(data); cleanupData(); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -51,7 +52,7 @@ async function addTagToContainer(containerName: string, tag: string) { try { const data = await readData(); const containerIndex = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -64,9 +65,9 @@ async function addTagToContainer(containerName: string, tag: string) { data.push({ name: containerName, tags: [tag] }); await saveData(data); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -74,19 +75,19 @@ async function removeTagFromContainer(containerName: string, tag: string) { try { const data = await readData(); const containerIndex = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (containerIndex !== -1 && data[containerIndex].tags) { data[containerIndex].tags = data[containerIndex].tags.filter( - (t: any) => t !== tag, + (t) => t !== tag, ); await saveData(data); cleanupData(); } - } catch (error: any) { + } catch (error: unknown) { logger.error(error); - throw new Error(error); + throw new Error(error as string); } } @@ -94,9 +95,9 @@ async function removeTagFromContainer(containerName: string, tag: string) { // Pin containers async function pinContainer(containerName: string) { try { - const data: any = await readData(); - const containerIndex: number = data.findIndex( - (container: any) => container.name === containerName, + const data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -106,9 +107,9 @@ async function pinContainer(containerName: string) { data.push({ name: containerName, pinned: true }); await saveData(data); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -116,7 +117,7 @@ async function unpinContainer(containerName: string) { try { const data = await readData(); const containerIndex = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -124,9 +125,9 @@ async function unpinContainer(containerName: string) { await saveData(data); cleanupData(); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -135,9 +136,9 @@ async function unpinContainer(containerName: string) { async function setLink(containerName: string, link: string) { if (link.match(regex)) { try { - const data: any = await readData(); - const containerIndex: any = data.findIndex( - (container: any) => container.name === containerName, + const data = await readData(); + const containerIndex = data.findIndex( + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -147,9 +148,9 @@ async function setLink(containerName: string, link: string) { data.push({ name: containerName, link: `${link}` }); await saveData(data); } - } catch (error: any) { + } catch (error: unknown) { logger.error(error); - throw new Error(error); + throw new Error(error as string); } } else { logger.error(`Provided link is not valid: ${link}`); @@ -161,7 +162,7 @@ async function removeLink(containerName: string) { try { const data = await readData(); const containerIndex = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -169,9 +170,9 @@ async function removeLink(containerName: string) { await saveData(data); cleanupData(); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -181,7 +182,7 @@ async function setIcon(containerName: string, icon: string, custom: boolean) { try { const data = await readData(); const containerIndex: number = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (custom === true) { @@ -199,9 +200,9 @@ async function setIcon(containerName: string, icon: string, custom: boolean) { data.push({ name: containerName, icon: `${icon}` }); await saveData(data); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -209,7 +210,7 @@ async function removeIcon(containerName: string) { try { const data = await readData(); const containerIndex = data.findIndex( - (container: any) => container.name === containerName, + (container) => container.name === containerName, ); if (containerIndex !== -1) { @@ -217,9 +218,9 @@ async function removeIcon(containerName: string) { await saveData(data); cleanupData(); } - } catch (error: any) { - logger.error(error); - throw new Error(error); + } catch (error: unknown) { + logger.error(error as Error); + throw new Error(error as string); } } @@ -227,11 +228,13 @@ async function removeIcon(containerName: string) { // Data specific functionss async function readData() { try { - const data = await fs.promises.readFile(dataPath, "utf-8"); - return JSON.parse(data); - } catch (error: any) { - console.error("readData"); - if (error.code === "ENOENT") { + const data: FrontendConfig = JSON.parse( + await fs.promises.readFile(dataPath, "utf-8"), + ); + return data; + } catch (error: unknown) { + console.error(`Error while reading ${dataPath}: ${error as Error}`); + if (error as Error) { await saveData([]); return []; } else { @@ -240,7 +243,7 @@ async function readData() { } } -async function saveData(data: any) { +async function saveData(data: FrontendConfig) { try { await fs.promises.writeFile( dataPath, @@ -248,15 +251,15 @@ async function saveData(data: any) { "utf-8", ); logger.info("Succesfully wrote to file"); - } catch (error: any) { - logger.error(error); + } catch (error: unknown) { + logger.error(error as Error); } } async function cleanupData() { try { const data = await readData(); - let cleanedData = []; + let cleanedData: FrontendConfig = []; if (data && Array.isArray(data)) { cleanedData = data.filter((container) => { @@ -273,8 +276,8 @@ async function cleanupData() { } await saveData(cleanedData); - } catch (error: any) { - logger.error(error); + } catch (error: unknown) { + logger.error(error as Error); } } diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index dd16bf6c..7bf7dc77 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -102,7 +102,7 @@ async function readConfig(): Promise { fs.readFileSync(haMasterPath, "utf-8"), ); return data; - } catch (error: any) { + } catch (error: unknown) { logger.error(`Error reading HA-Config: ${(error as Error).message}`); return null; } finally { diff --git a/src/controllers/notificationController.ts b/src/controllers/notificationController.ts index ad0b1bcc..0ece9553 100644 --- a/src/controllers/notificationController.ts +++ b/src/controllers/notificationController.ts @@ -56,3 +56,5 @@ async function sendNotification(containerId: string) { notify("whatsapp", containerId); } } + +export default sendNotification; diff --git a/src/controllers/scheduler.ts b/src/controllers/scheduler.ts index 763b67f9..caa19481 100644 --- a/src/controllers/scheduler.ts +++ b/src/controllers/scheduler.ts @@ -11,7 +11,7 @@ const scheduleFetch = () => { try { fetchData(); cleanupOldEntries(); - } catch (error: any) { + } catch (error: unknown) { logger.error(`Error during scheduled fetch: ${error}`); } @@ -81,8 +81,8 @@ const cleanupOldEntries = async () => { try { db.run("DELETE FROM data WHERE timestamp < ?", twentyFourHoursAgo, Error); logger.info("Old entries cleared from the database."); - } catch (Error: any) { - logger.error(`Error clearing old entries: ${Error.message}`); + } catch (Error: unknown) { + logger.error(`Error clearing old entries: ${(Error as Error).message}`); } }; diff --git a/src/data/frontendConfiguration.json b/src/data/frontendConfiguration.json index 4697f960..884e0e20 100644 --- a/src/data/frontendConfiguration.json +++ b/src/data/frontendConfiguration.json @@ -2,7 +2,15 @@ { "name": "test", "tags": [ - "123" - ] + "123", + "123", + "321" + ], + "link": "https://google.com", + "icon": "custom/test.png" + }, + { + "name": "test2", + "pinned": true } ] \ No newline at end of file diff --git a/src/init.ts b/src/init.ts index 119950c0..8c757379 100644 --- a/src/init.ts +++ b/src/init.ts @@ -27,7 +27,7 @@ const initializeApp = (app: express.Application): void => { next(), ); - swaggerDocs(app as any); + swaggerDocs(app); trustedProxies(app); scheduleFetch(); diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts index 08ffd219..500a7fa8 100644 --- a/src/middleware/authMiddleware.ts +++ b/src/middleware/authMiddleware.ts @@ -43,7 +43,7 @@ async function authMiddleware( logger.debug("Authentication succesfull"); next(); - } catch (error: any) { + } catch (error: unknown) { logger.error("Error in authMiddleware:", error); res.status(500).json({ message: "Internal server error" }); } diff --git a/src/routes/auth/routes.ts b/src/routes/auth/routes.ts index 4af13884..f7e0b18e 100644 --- a/src/routes/auth/routes.ts +++ b/src/routes/auth/routes.ts @@ -7,11 +7,6 @@ const passwordBool: string = "./src/data/usePassword.txt"; const saltRounds: number = 10; const router: Router = Router(); -let passwordData: { - hash: string; - salt: string; -}; - async function authEnabled(): Promise { let isAuthEnabled: boolean = false; let data: string = ""; @@ -19,8 +14,8 @@ async function authEnabled(): Promise { data = await fs.readFile(passwordBool, "utf8"); isAuthEnabled = data.trim() === "true"; return isAuthEnabled; - } catch (error: any) { - logger.error("Error reading file: ", error); + } catch (error: unknown) { + logger.error("Error reading file: ", error as Error); return isAuthEnabled; } } @@ -30,8 +25,8 @@ async function readPasswordFile() { try { data = await fs.readFile(passwordFile, "utf8"); return data; - } catch (error: any) { - logger.error("Could not read saved password: ", error); + } catch (error: unknown) { + logger.error("Could not read saved password: ", error as Error); return data; } } @@ -42,8 +37,8 @@ async function writePasswordFile(passwordData: string) { setTrue(); logger.debug("Authentication enabled"); return "Authentication enabled"; - } catch (error: any) { - logger.error("Error writing password file:", error); + } catch (error: unknown) { + logger.error("Error writing password file:", error as Error); return error; } } @@ -53,8 +48,8 @@ async function setTrue() { await fs.writeFile(passwordBool, "true", "utf8"); logger.info(`Enabled authentication`); return; - } catch (error: any) { - logger.error("Error writing to the file:", error); + } catch (error: unknown) { + logger.error("Error writing to the file:", error as Error); return; } } @@ -64,8 +59,8 @@ async function setFalse() { await fs.writeFile(passwordBool, "false", "utf8"); logger.info(`Disabled authentication`); return; - } catch (error: any) { - logger.error("Error writing to the file:", error); + } catch (error: unknown) { + logger.error("Error writing to the file:", error as Error); return; } } @@ -118,8 +113,8 @@ router.post("/enable", async (req: Request, res: Response): Promise => { res .status(200) .json({ message: "Password Authentication enabled successfully" }); - } catch (error) { - logger.error(`Error enabling password authentication: ${error}`); + } catch (error: unknown) { + logger.error(`Error enabling password authentication: ${error as Error}`); res.status(500).json({ message: "An error occurred" }); } }); @@ -165,8 +160,8 @@ router.post("/disable", async (req: Request, res: Response): Promise => { await setFalse(); // Assuming this is an async function res.status(200).json({ message: "Authentication disabled" }); - } catch (error) { - logger.error(`Error disabling authentication: ${error}`); + } catch (error: unknown) { + logger.error(`Error disabling authentication: ${error as Error}`); res.status(500).json({ message: "An error occurred" }); } }); diff --git a/src/routes/data/routes.ts b/src/routes/data/routes.ts index 0e9a6e36..108fafe4 100644 --- a/src/routes/data/routes.ts +++ b/src/routes/data/routes.ts @@ -2,14 +2,19 @@ import express from "express"; const router = express.Router(); import db from "../../config/db"; import logger from "../../utils/logger"; +import Table from "../../typings/table"; interface DataRow { info: string; } -function formatRows(rows: DataRow[]): Record { +function formatRows(rows: DataRow[]): Record { return rows.reduce( - (acc: Record, row, index: number): Record => { + ( + acc: Record, + row, + index: number, + ): Record => { acc[index] = JSON.parse(row.info); return acc; }, @@ -88,26 +93,31 @@ function formatRows(rows: DataRow[]): Record { router.get("/latest", (req, res) => { db.get( "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", - (error, row: any) => { + (error: unknown, row: Partial> | undefined) => { if (error) { - logger.error("Error fetching latest data:", error.message); + logger.error("Error fetching latest data:", (error as Error).message); return res.status(500).json({ error: "Internal server error" }); } - if (!row) { + if (!row || !row.info) { logger.warn("No data available for /data/latest"); return res.status(404).json({ error: "No data available" }); } logger.debug("Fetching /data/latest"); - res.json(JSON.parse(row.info)); + try { + res.json(JSON.parse(row.info)); + } catch (error: unknown) { + logger.error("Error parsing data:", (error as Error).message); + res.status(500).json({ error: "Data format error" }); + } }, ); }); /** * @swagger - * /data/time/24h: + * /data/all: * get: * summary: Retrieve container statistics entries from the last 24 hours * tags: [Database queries] @@ -152,17 +162,27 @@ router.get("/latest", (req, res) => { * type: number * example: 3072 */ -router.get("/time/24h", (req, res) => { +router.get("/all", (req, res) => { const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + db.all( "SELECT info FROM data WHERE timestamp >= ?", [oneDayAgo], - (error, rows: DataRow[]) => { + (error: unknown, rows: Pick[] | undefined) => { if (error) { - logger.error("Error fetching data from last 24 hours:", error.message); + logger.error( + "Error fetching data from last 24 hours:", + (error as Error).message, + ); return res.status(500).json({ error: "Internal server error" }); } + logger.debug("Fetching /data/time/24h"); + if (!rows || rows.length === 0) { + logger.warn("No data available for /data/time/24h"); + return res.status(404).json({ error: "No data available" }); + } + res.json(formatRows(rows)); }, ); @@ -188,9 +208,9 @@ router.get("/time/24h", (req, res) => { * example: "Database cleared successfully." */ router.delete("/clear", (req, res) => { - db.run("DELETE FROM data", (err) => { - if (err) { - logger.error("Error clearing the database:", err.message); + db.run("DELETE FROM data", (error: unknown) => { + if (error) { + logger.error("Error clearing the database:", (error as Error).message); return res.status(500).json({ error: "Internal server error" }); } logger.debug("Database cleared successfully"); diff --git a/src/routes/frontendController/routes.ts b/src/routes/frontendController/routes.ts index 0fce63e0..0de95fe9 100644 --- a/src/routes/frontendController/routes.ts +++ b/src/routes/frontendController/routes.ts @@ -68,8 +68,8 @@ router.post("/show/:containerName", async (req, res) => { try { await unhideContainer(containerName); res.status(200).json({ message: "Container unhidden successfully." }); - } catch (error: any) { - res.status(500).json({ error: error.message }); + } catch (error: unknown) { + res.status(500).json({ error: (error as Error).message }); } }); @@ -126,8 +126,8 @@ router.post("/tag/:containerName/:tag", async (req, res) => { try { await addTagToContainer(containerName, tag); res.json({ success: true, message: "Tag added successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); @@ -178,8 +178,8 @@ router.post("/pin/:containerName", async (req, res) => { try { await pinContainer(containerName); res.json({ success: true, message: "Container pinned successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); @@ -236,8 +236,8 @@ router.post("/add-link/:containerName/:link", async (req, res) => { try { await setLink(containerName, link); res.json({ success: true, message: "Link added successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); @@ -304,8 +304,8 @@ router.post( await setIcon(containerName, icon, custom); res.json({ success: true, message: "Icon added successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }, ); @@ -366,8 +366,8 @@ router.delete("/hide/:containerName", async (req, res) => { try { await hideContainer(target); res.json({ success: true, message: `Container, ${target}, hidden.` }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); @@ -424,8 +424,8 @@ router.delete("/remove-tag/:containerName/:tag", async (req, res) => { try { await removeTagFromContainer(containerName, tag); res.json({ success: true, message: "Tag removed successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); @@ -476,8 +476,8 @@ router.delete("/unpin/:containerName", async (req, res) => { try { await unpinContainer(containerName); res.json({ success: true, message: "Container unpinned successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); @@ -528,8 +528,8 @@ router.delete("/remove-link/:containerName", async (req, res) => { try { await removeLink(containerName); res.json({ success: true, message: "Link removed successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); @@ -580,8 +580,8 @@ router.delete("/remove-icon/:containerName", async (req, res) => { try { await removeIcon(containerName); res.json({ success: true, message: "Icon removed successfully." }); - } catch (error: any) { - res.status(500).json({ success: false, error: error.message }); + } catch (error: unknown) { + res.status(500).json({ success: false, error: (error as Error).message }); } }); diff --git a/src/routes/getter/routes.ts b/src/routes/getter/routes.ts index 8e3c6955..d278075c 100644 --- a/src/routes/getter/routes.ts +++ b/src/routes/getter/routes.ts @@ -1,6 +1,5 @@ import extractRelevantData from "../../utils/extractHostData"; import { Router, Request, Response } from "express"; -import { writeOfflineLog, readOfflineLog } from "../../utils/writeOfflineLog"; import getDockerClient from "../../utils/dockerClient"; import fetchAllContainers from "../../utils/containerService"; import { getCurrentSchedule } from "../../controllers/scheduler"; @@ -10,6 +9,7 @@ import checkReachability from "../../utils/connectionChecker"; const configPath = "./src/data/dockerConfig.json"; const router = Router(); const userConf = "./src/data/user.conf"; +import { dockerConfig } from "../../typings/dockerConfig"; /** * @swagger @@ -35,17 +35,17 @@ router.get("/hosts", (req: Request, res: Response) => { logger.info(`Fetching config: ${configPath}`); try { const rawData = fs.readFileSync(configPath, "utf-8"); - const config = JSON.parse(rawData); + const config: dockerConfig = JSON.parse(rawData); if (!config.hosts) { throw new Error("No hosts defined in configuration."); } - const hosts = config.hosts.map((host: any) => host.name); + const hosts = config.hosts.map((host) => host.name); logger.debug("Fetching all available Docker hosts"); res.status(200).json({ hosts }); - } catch (error: any) { - logger.error("Error fetching hosts: " + error.message); + } catch (error: unknown) { + logger.error("Error fetching hosts: " + (error as Error).message); res.status(500).json({ error: "Failed to fetch Docker hosts" }); } }); @@ -86,8 +86,8 @@ router.get("/system", (req: Request, res: Response) => { res.status(500).json({ error: `Error received empty ${userConf}` }); } res.status(200).json(config); - } catch (error: any) { - logger.error(`Could not fetch ${userConf}: ${error}`); + } catch (error: unknown) { + logger.error(`Could not fetch ${userConf}: ${error as Error}`); res.status(500).json({ error: `Failed to fetch ${userConf}` }); } }); @@ -142,14 +142,13 @@ router.get("/host/:hostName/stats", async (req: Request, res: Response) => { const version = await docker.version(); const relevantData = extractRelevantData({ hostName, info, version }); - writeOfflineLog(JSON.stringify(relevantData)); res.status(200).json(relevantData); - } catch (error: any) { + } catch (error: unknown) { logger.error( - `Error fetching stats for host: ${hostName} - ${error.message || "Unknown error"}`, + `Error fetching stats for host: ${hostName} - ${(error as Error).message || "Unknown error"}`, ); res.status(500).json({ - error: `Error fetching host stats: ${error.message || "Unknown error"}`, + error: `Error fetching host stats: ${(error as Error).message || "Unknown error"}`, }); } }); @@ -233,8 +232,8 @@ router.get("/containers", async (req: Request, res: Response) => { const allContainerData = await fetchAllContainers(); logger.debug("Fetched /api/containers"); res.status(200).json(allContainerData); - } catch (error: any) { - logger.error(`Error fetching containers: ${error.message}`); + } catch (error: unknown) { + logger.error(`Error fetching containers: ${(error as Error).message}`); res.status(500).json({ error: "Failed to fetch containers" }); } }); @@ -270,8 +269,10 @@ router.get("/config", async (req: Request, res: Response) => { const jsonData = JSON.parse(rawData.toString()); logger.debug("Fetching /api/config"); res.status(200).json(jsonData); - } catch (error: any) { - logger.error("Error loading dockerConfig.json: " + error.message); + } catch (error: unknown) { + logger.error( + "Error loading dockerConfig.json: " + (error as Error).message, + ); res.status(500).json({ error: "Failed to load Docker configuration" }); } }); @@ -334,8 +335,8 @@ router.get("/status", async (req: Request, res: Response) => { try { const jsonData = await checkReachability(); res.status(200).json(jsonData); - } catch (error: any) { - logger.error(`Error while fetching data: ${error}`); + } catch (error: unknown) { + logger.error(`Error while fetching data: ${error as Error}`); } }); @@ -398,8 +399,10 @@ router.get("/frontend-config", (req: Request, res: Response) => { const jsonData = JSON.parse(rawData.toString()); res.status(200).json(jsonData); - } catch (error: any) { - logger.error("Error loading frontendConfiguration.json: " + error.message); + } catch (error: unknown) { + logger.error( + "Error loading frontendConfiguration.json: " + (error as Error).message, + ); res.status(500).json({ error: "Failed to load Frontend configuration" }); } }); diff --git a/src/routes/notifications/routes.ts b/src/routes/notifications/routes.ts index 262d48f3..17cf6986 100644 --- a/src/routes/notifications/routes.ts +++ b/src/routes/notifications/routes.ts @@ -12,7 +12,7 @@ interface TemplateData { text: string; } -function isTemplateData(data: any): data is TemplateData { +function isTemplateData(data: TemplateData): data is TemplateData { return ( data !== null && typeof data === "object" && typeof data.text === "string" ); @@ -169,8 +169,8 @@ router.post("/test/:type/:containerId", async (req: Request, res: Response) => { try { await notify(type, containerId); res.json({ success: true, message: `Sent test notification to ${type}` }); - } catch (error: any) { - res.json({ success: false, message: `Errored: ${error}` }); + } catch (error: unknown) { + res.json({ success: false, message: `Errored: ${error as Error}` }); } }); diff --git a/src/routes/setter/routes.ts b/src/routes/setter/routes.ts index fcffeef9..96915a92 100644 --- a/src/routes/setter/routes.ts +++ b/src/routes/setter/routes.ts @@ -1,9 +1,9 @@ import { setFetchInterval, parseInterval } from "../../controllers/scheduler"; import logger from "../../utils/logger"; -import { Router, Request, Response } from "express"; +import express, { Router, Request, Response } from "express"; import fs from "fs"; -const router = Router(); +const router: Router = express.Router(); const configPath: string = "./src/data/dockerConfig.json"; interface Host { @@ -101,20 +101,20 @@ router.put( * 400: * description: Invalid interval format or out of range. */ -router.put("/scheduler", (req: any, res: any) => { +router.put("/scheduler", (req: Request, res: Response) => { const interval = req.query.interval as string; try { const newInterval = parseInterval(interval); if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { - return res + res .status(400) .json({ error: "Interval must be between 5 minutes and 6 hours." }); } setFetchInterval(newInterval); - res.json({ message: `Fetch interval set to ${interval}.` }); + res.status(200).json({ message: `Fetch interval set to ${interval}.` }); } catch (error: unknown) { const err = error as Error; logger.error("Error setting fetch interval: " + err.message); diff --git a/src/typings/dockerConfig.ts b/src/typings/dockerConfig.ts new file mode 100644 index 00000000..fea0f4ec --- /dev/null +++ b/src/typings/dockerConfig.ts @@ -0,0 +1,10 @@ +interface target { + name: string; + url: string; + port: number; +} + +interface dockerConfig { + hosts: target[]; +} +export { dockerConfig, target }; diff --git a/src/typings/frontendConfig.ts b/src/typings/frontendConfig.ts new file mode 100644 index 00000000..6ce14979 --- /dev/null +++ b/src/typings/frontendConfig.ts @@ -0,0 +1,12 @@ +interface Container { + name: string; + hidden?: boolean; + tags?: string[]; + link?: string; + icon?: string; + pinned?: boolean; +} + +type FrontendConfig = Container[]; + +export { FrontendConfig }; diff --git a/src/typings/states.ts b/src/typings/states.ts new file mode 100644 index 00000000..d5eed20b --- /dev/null +++ b/src/typings/states.ts @@ -0,0 +1,10 @@ +interface Container { + name: string; + id: string; + state: string; + hostName: string; +} + +type ContainerStates = Container[]; + +export { ContainerStates, Container }; diff --git a/src/typings/table.ts b/src/typings/table.ts new file mode 100644 index 00000000..4845ebaa --- /dev/null +++ b/src/typings/table.ts @@ -0,0 +1,7 @@ +type Table = { + id: number; // Primary key, auto-incremented + info: string; // Non-null text field + timestamp: string; // ISO 8601 formatted datetime string +}; + +export default Table; diff --git a/src/utils/connectionChecker.ts b/src/utils/connectionChecker.ts index 289b9b37..92efba3d 100644 --- a/src/utils/connectionChecker.ts +++ b/src/utils/connectionChecker.ts @@ -67,8 +67,8 @@ async function checkReachability(): Promise { const parsedData = JSON.parse(data); const hosts: Host[] = parsedData.hosts; return await checkHostStatus(hosts); - } catch (error: any) { - logger.error(`Error reading file: ${error}`); + } catch (error: unknown) { + logger.error(`Error reading file: ${error as Error}`); return undefined; } } diff --git a/src/utils/containerService.ts b/src/utils/containerService.ts index 0cd09e39..841e9c2b 100644 --- a/src/utils/containerService.ts +++ b/src/utils/containerService.ts @@ -6,7 +6,7 @@ const configPath = "./src/data/dockerConfig.json"; interface HostConfig { name: string; - [key: string]: any; + [key: string]: string | number; } interface ContainerData { @@ -44,8 +44,8 @@ function loadConfig() { const configData = fs.readFileSync(configPath, "utf-8"); logger.debug("Loaded " + configPath); return JSON.parse(configData); - } catch (error: any) { - logger.error(`Failed to load config: ${error.message}`); + } catch (error: unknown) { + logger.error(`Failed to load config: ${(error as Error).message}`); return null; } } @@ -62,7 +62,7 @@ async function fetchAllContainers(): Promise { for (const hostConfig of config.hosts as HostConfig[]) { const hostName = hostConfig.name; try { - const docker: any = getDockerClient(hostName); + const docker = getDockerClient(hostName); logger.debug(`Now processing: ${hostName}`); const containers: ContainerInfo[] = await docker.listContainers({ all: true, @@ -103,9 +103,9 @@ async function fetchAllContainers(): Promise { current_net_tx: containerStats.networks?.eth0?.tx_bytes || 0, networkMode: containerInfo.HostConfig.NetworkMode || "unknown", }; - } catch (containerError: any) { + } catch (containerError: unknown) { logger.error( - `Error fetching details for container ID: ${container.Id} on host: ${hostName} - ${containerError.message}`, + `Error fetching details for container ID: ${container.Id} on host: ${hostName} - ${(containerError as Error).message}`, ); return { name: container.Names[0].replace("/", ""), @@ -124,12 +124,12 @@ async function fetchAllContainers(): Promise { } }), ); - } catch (error: any) { + } catch (error: unknown) { logger.error( - `Error fetching containers for host: ${hostName} - ${error.message}. Stack: ${error.stack}`, + `Error fetching containers for host: ${hostName} - ${(error as Error).message}. Stack: ${(error as Error).stack}`, ); allContainerData[hostName] = { - error: `Error fetching containers: ${error.message}`, + error: `Error fetching containers: ${(error as Error).message}`, }; } } diff --git a/src/utils/dockerClient.ts b/src/utils/dockerClient.ts index 4cb3f70c..dc0f5e91 100644 --- a/src/utils/dockerClient.ts +++ b/src/utils/dockerClient.ts @@ -19,7 +19,7 @@ function loadDockerConfig(): DockerConfig { const rawData = fs.readFileSync(configPath, "utf-8"); logger.debug("Refreshed DockerConfig.json"); return JSON.parse(rawData) as DockerConfig; - } catch (error: any) { + } catch (error: unknown) { logger.error( "Error loading dockerConfig.json: " + (error as Error).message, ); diff --git a/src/utils/notifications/_notify.ts b/src/utils/notifications/_notify.ts index 139a0066..49717f90 100644 --- a/src/utils/notifications/_notify.ts +++ b/src/utils/notifications/_notify.ts @@ -43,9 +43,8 @@ async function notify(type: string, containerId: string) { await pushoverNotification(containerId); break; default: - const errorMsg = "Unknown notification type."; - logger.error(errorMsg); - throw new Error(errorMsg); + logger.error("Unknown notification type."); + throw new Error("Unknown notification type."); } } diff --git a/src/utils/notifications/_template.ts b/src/utils/notifications/_template.ts index 551da826..250f0950 100644 --- a/src/utils/notifications/_template.ts +++ b/src/utils/notifications/_template.ts @@ -1,5 +1,6 @@ import fs from "fs"; import logger from "../logger"; +import { ContainerStates, Container } from "../../typings/states"; const templatePath: string = "./src/data/template.json"; const containersPath: string = "./src/data/states.json"; @@ -12,8 +13,8 @@ function getTemplate(): Template | null { try { const data = fs.readFileSync(templatePath, "utf8"); return JSON.parse(data); - } catch (error: any) { - logger.error("Failed to load template:", error); + } catch (error: unknown) { + logger.error("Failed to load template:", error as Error); return null; } } @@ -26,8 +27,8 @@ function setTemplate(newTemplate: string): void { "utf8", ); logger.debug("Template updated successfully"); - } catch (error: any) { - logger.error("Failed to update template:", error); + } catch (error: unknown) { + logger.error("Failed to update template:", error as Error); } } @@ -42,9 +43,11 @@ function renderTemplate(containerId: string): string | null { const data = fs.readFileSync(containersPath, "utf8"); const containers = JSON.parse(data); - let containerData: Record | null = null; + let containerData: ContainerStates | null = null; for (const host in containers) { - containerData = containers[host].find((c: any) => c.id === containerId); + containerData = containers[host].find( + (c: Container) => c.id === containerId, + ); if (containerData) { break; } @@ -56,13 +59,13 @@ function renderTemplate(containerId: string): string | null { } // Substitute placeholders in the template with container data - return Object.keys(containerData).reduce( - (text, key) => - text.replace(new RegExp(`{{${key}}}`, "g"), containerData[key]), - template.text, - ); - } catch (error: any) { - logger.error("Failed to load containers:", error); + return Object.keys(containerData).reduce((text, key) => { + const value = containerData[key as keyof ContainerStates]; + // Convert value to a string to avoid errors + return text.replace(new RegExp(`{{${key}}}`, "g"), String(value)); + }, template.text); + } catch (error: unknown) { + logger.error("Failed to load containers:", error as Error); return null; } } diff --git a/src/utils/notifications/email.ts b/src/utils/notifications/email.ts index 57c94ef9..4cd41a10 100644 --- a/src/utils/notifications/email.ts +++ b/src/utils/notifications/email.ts @@ -46,7 +46,7 @@ export async function emailNotification(containerId: string) { try { await transporter.sendMail(mailOptions); - } catch (error: any) { - logger.error("Error sending email:", error); + } catch (error: unknown) { + logger.error("Error sending email:", error as Error); } } diff --git a/src/utils/swaggerDocs.ts b/src/utils/swaggerDocs.ts index 9a386ddd..540304a7 100644 --- a/src/utils/swaggerDocs.ts +++ b/src/utils/swaggerDocs.ts @@ -1,9 +1,9 @@ import swaggerUi from "swagger-ui-express"; import swaggerJsdoc from "swagger-jsdoc"; import swaggerConfig from "../config/swaggerConfig"; -import { Express } from "express"; +import express from "express"; -const swaggerDocs = (app: Express) => { +const swaggerDocs = (app: express.Application) => { const specs = swaggerJsdoc(swaggerConfig); app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs)); }; diff --git a/src/utils/writeOfflineLog.ts b/src/utils/writeOfflineLog.ts deleted file mode 100644 index 244f62e0..00000000 --- a/src/utils/writeOfflineLog.ts +++ /dev/null @@ -1,26 +0,0 @@ -import fs from "fs"; -import logger from "../utils/logger"; - -const LOG_FILE_PATH = "./logs/hostStats.json"; - -async function writeOfflineLog(message: string) { - try { - if (!fs.existsSync(LOG_FILE_PATH)) { - await fs.promises.writeFile(LOG_FILE_PATH, message); - } - } catch (error: any) { - logger.error("Error writing one time reference log: ", error); - } -} - -async function readOfflineLog() { - try { - const data = await fs.promises.readFile(LOG_FILE_PATH, "utf-8"); - logger.debug("Returning data:", data); - return data; - } catch (error: any) { - logger.error("Error reading offline log:", error); - } -} - -export { writeOfflineLog, readOfflineLog }; diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 9c800499..00000000 --- a/yarn.lock +++ /dev/null @@ -1,2852 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@apidevtools/json-schema-ref-parser@^9.0.6": - version "9.1.2" - resolved "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz" - integrity sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg== - dependencies: - "@jsdevtools/ono" "^7.1.3" - "@types/json-schema" "^7.0.6" - call-me-maybe "^1.0.1" - js-yaml "^4.1.0" - -"@apidevtools/openapi-schemas@^2.0.4": - version "2.1.0" - resolved "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz" - integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== - -"@apidevtools/swagger-methods@^3.0.2": - version "3.0.2" - resolved "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz" - integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== - -"@apidevtools/swagger-parser@10.0.3": - version "10.0.3" - resolved "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz" - integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== - dependencies: - "@apidevtools/json-schema-ref-parser" "^9.0.6" - "@apidevtools/openapi-schemas" "^2.0.4" - "@apidevtools/swagger-methods" "^3.0.2" - "@jsdevtools/ono" "^7.1.3" - call-me-maybe "^1.0.1" - z-schema "^5.0.1" - -"@balena/dockerignore@^1.0.2": - version "1.0.2" - resolved "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz" - integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q== - -"@colors/colors@^1.6.0", "@colors/colors@1.6.0": - version "1.6.0" - resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz" - integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== - -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" - -"@dabh/diagnostics@^2.0.2": - version "2.0.3" - resolved "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz" - integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== - dependencies: - colorspace "1.1.x" - enabled "2.0.x" - kuler "^2.0.0" - -"@esbuild/linux-x64@0.23.1": - version "0.23.1" - resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz" - integrity sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ== - -"@gar/promisify@^1.0.1": - version "1.1.3" - resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz" - integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== - -"@jridgewell/resolve-uri@^3.0.3": - version "3.1.2" - resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/sourcemap-codec@^1.4.10": - version "1.5.0" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== - -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jsdevtools/ono@^7.1.3": - version "7.1.3" - resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz" - integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== - -"@mapbox/node-pre-gyp@^1.0.11": - version "1.0.11" - resolved "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz" - integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ== - dependencies: - detect-libc "^2.0.0" - https-proxy-agent "^5.0.0" - make-dir "^3.1.0" - node-fetch "^2.6.7" - nopt "^5.0.0" - npmlog "^5.0.1" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.11" - -"@npmcli/fs@^1.0.0": - version "1.1.1" - resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz" - integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== - dependencies: - "@gar/promisify" "^1.0.1" - semver "^7.3.5" - -"@npmcli/move-file@^1.0.1": - version "1.1.2" - resolved "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz" - integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== - dependencies: - mkdirp "^1.0.4" - rimraf "^3.0.2" - -"@playwright/test@^1.49.0": - version "1.49.0" - resolved "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz" - integrity sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw== - dependencies: - playwright "1.49.0" - -"@scarf/scarf@=1.4.0": - version "1.4.0" - resolved "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz" - integrity sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ== - -"@tootallnate/once@1": - version "1.1.2" - resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== - -"@tsconfig/node10@^1.0.7": - version "1.0.11" - resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz" - integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.4" - resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz" - integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== - -"@types/bcrypt@^5.0.2": - version "5.0.2" - resolved "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz" - integrity sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ== - dependencies: - "@types/node" "*" - -"@types/body-parser@*": - version "1.19.5" - resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz" - integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.38" - resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" - integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== - dependencies: - "@types/node" "*" - -"@types/cors@^2.8.17": - version "2.8.17" - resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz" - integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== - dependencies: - "@types/node" "*" - -"@types/docker-modem@*": - version "3.0.6" - resolved "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz" - integrity sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg== - dependencies: - "@types/node" "*" - "@types/ssh2" "*" - -"@types/dockerode@^3.3.31": - version "3.3.32" - resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.32.tgz" - integrity sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg== - dependencies: - "@types/docker-modem" "*" - "@types/node" "*" - "@types/ssh2" "*" - -"@types/express-handlebars@^5.3.1": - version "5.3.1" - resolved "https://registry.npmjs.org/@types/express-handlebars/-/express-handlebars-5.3.1.tgz" - integrity sha512-DSzaERLO4gHb8AqnrL58jzSDyT0yDdl6HqDc+bGz1Hf0nrG1FK30nHGzv8NBEGR8QV9eUGB/YaE0Qj3NjF7siw== - -"@types/express-serve-static-core@^5.0.0": - version "5.0.2" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz" - integrity sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - "@types/send" "*" - -"@types/express@*", "@types/express@^5.0.0": - version "5.0.0" - resolved "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz" - integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^5.0.0" - "@types/qs" "*" - "@types/serve-static" "*" - -"@types/http-errors@*": - version "2.0.4" - resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz" - integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== - -"@types/json-schema@^7.0.6": - version "7.0.15" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - -"@types/mime@^1": - version "1.3.5" - resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" - integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== - -"@types/node-fetch@^2.6.12": - version "2.6.12" - resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz" - integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA== - dependencies: - "@types/node" "*" - form-data "^4.0.0" - -"@types/node@*", "@types/node@^22.9.0": - version "22.10.1" - resolved "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz" - integrity sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ== - dependencies: - undici-types "~6.20.0" - -"@types/node@^18.11.18": - version "18.19.67" - resolved "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz" - integrity sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ== - dependencies: - undici-types "~5.26.4" - -"@types/nodemailer@^6.4.17": - version "6.4.17" - resolved "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz" - integrity sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww== - dependencies: - "@types/node" "*" - -"@types/qs@*": - version "6.9.17" - resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz" - integrity sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ== - -"@types/range-parser@*": - version "1.2.7" - resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" - integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== - -"@types/send@*": - version "0.17.4" - resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz" - integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== - dependencies: - "@types/mime" "^1" - "@types/node" "*" - -"@types/serve-static@*": - version "1.15.7" - resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz" - integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== - dependencies: - "@types/http-errors" "*" - "@types/node" "*" - "@types/send" "*" - -"@types/ssh2@*": - version "1.15.1" - resolved "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz" - integrity sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA== - dependencies: - "@types/node" "^18.11.18" - -"@types/supports-color@^8.1.3": - version "8.1.3" - resolved "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz" - integrity sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg== - -"@types/swagger-jsdoc@^6.0.4": - version "6.0.4" - resolved "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz" - integrity sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ== - -"@types/swagger-ui-express@^4.1.7": - version "4.1.7" - resolved "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz" - integrity sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g== - dependencies: - "@types/express" "*" - "@types/serve-static" "*" - -"@types/triple-beam@^1.3.2": - version "1.3.5" - resolved "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz" - integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== - -abbrev@1: - version "1.1.1" - resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - -acorn-jsx-walk@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz" - integrity sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA== - -acorn-jsx@^5.3.2: - version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn-loose@^8.4.0: - version "8.4.0" - resolved "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz" - integrity sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ== - dependencies: - acorn "^8.11.0" - -acorn-walk@^8.1.1, acorn-walk@^8.3.4: - version "8.3.4" - resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz" - integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== - dependencies: - acorn "^8.11.0" - -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.11.0, acorn@^8.14.0, acorn@^8.4.1: - version "8.14.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz" - integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== - -agent-base@^6.0.2, agent-base@6: - version "6.0.2" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -agentkeepalive@^4.1.3: - version "4.5.0" - resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz" - integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== - dependencies: - humanize-ms "^1.2.1" - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv@^8.17.1: - version "8.17.1" - resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" - integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== - dependencies: - fast-deep-equal "^3.1.3" - fast-uri "^3.0.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-regex@^6.0.1: - version "6.1.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz" - integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== - -ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -"aproba@^1.0.3 || ^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz" - integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== - -are-we-there-yet@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz" - integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" - -are-we-there-yet@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz" - integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" - -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - -asn1@^0.2.6: - version "0.2.6" - resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - -async@^3.2.3: - version "3.2.6" - resolved "https://registry.npmjs.org/async/-/async-3.2.6.tgz" - integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -bcrypt-pbkdf@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" - integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== - dependencies: - tweetnacl "^0.14.3" - -bcrypt@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz" - integrity sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww== - dependencies: - "@mapbox/node-pre-gyp" "^1.0.11" - node-addon-api "^5.0.0" - -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bl@^4.0.3: - version "4.1.0" - resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -body-parser@1.20.3: - version "1.20.3" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz" - integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== - dependencies: - bytes "3.1.2" - content-type "~1.0.5" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.13.0" - raw-body "2.5.2" - type-is "~1.6.18" - unpipe "1.0.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@~3.0.2: - version "3.0.3" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -buildcheck@~0.0.6: - version "0.0.6" - resolved "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz" - integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A== - -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -cacache@^15.2.0: - version "15.3.0" - resolved "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz" - integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== - dependencies: - "@npmcli/fs" "^1.0.0" - "@npmcli/move-file" "^1.0.1" - chownr "^2.0.0" - fs-minipass "^2.0.0" - glob "^7.1.4" - infer-owner "^1.0.4" - lru-cache "^6.0.0" - minipass "^3.1.1" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.2" - mkdirp "^1.0.3" - p-map "^4.0.0" - promise-inflight "^1.0.1" - rimraf "^3.0.2" - ssri "^8.0.1" - tar "^6.0.2" - unique-filename "^1.1.1" - -call-bind-apply-helpers@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz" - integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -call-bind@^1.0.7: - version "1.0.8" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz" - integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-define-property "^1.0.0" - get-intrinsic "^1.2.4" - set-function-length "^1.2.2" - -call-me-maybe@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz" - integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== - -chalk@^4.1.0: - version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@^5.3.0: - version "5.3.0" - resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" - integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== - -chokidar@^3.5.2: - version "3.6.0" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chokidar@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz" - integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== - dependencies: - readdirp "^4.0.1" - -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-cursor@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz" - integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw== - dependencies: - restore-cursor "^5.0.0" - -cli-spinners@^2.9.2: - version "2.9.2" - resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz" - integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== - -color-convert@^1.9.3: - version "1.9.3" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@^1.0.0, color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^1.6.0: - version "1.9.1" - resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz" - integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color-support@^1.1.2, color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - -color@^3.1.3: - version "3.2.1" - resolved "https://registry.npmjs.org/color/-/color-3.2.1.tgz" - integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== - dependencies: - color-convert "^1.9.3" - color-string "^1.6.0" - -colorspace@1.1.x: - version "1.1.4" - resolved "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz" - integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== - dependencies: - color "^3.1.3" - text-hex "1.0.x" - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@^12.1.0: - version "12.1.0" - resolved "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz" - integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== - -commander@^9.4.1: - version "9.5.0" - resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz" - integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== - -commander@6.2.0: - version "6.2.0" - resolved "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz" - integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -console-control-strings@^1.0.0, console-control-strings@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" - integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== - -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - -content-type@~1.0.4, content-type@~1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== - -cookie@0.7.1: - version "0.7.1" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz" - integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== - -cors@^2.8.5: - version "2.8.5" - resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" - integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== - dependencies: - object-assign "^4" - vary "^1" - -cpu-features@~0.0.10: - version "0.0.10" - resolved "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz" - integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA== - dependencies: - buildcheck "~0.0.6" - nan "^2.19.0" - -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - -data-uri-to-buffer@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz" - integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== - -debug@^4, debug@^4.1.1, debug@^4.3.3, debug@4: - version "4.4.0" - resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz" - integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== - dependencies: - ms "^2.1.3" - -debug@2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -decompress-response@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz" - integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== - dependencies: - mimic-response "^3.1.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" - integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== - -depd@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -dependency-cruiser@^16.5.0: - version "16.7.0" - resolved "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.7.0.tgz" - integrity sha512-522LLjHINl9r0RIZ8/6s6TqIHTuEJG3XDU2WPSm9dG0rvLUYVyQwE9ID31tDFs4OOyEhdOPaqAaAG1jRv/Zwbg== - dependencies: - acorn "^8.14.0" - acorn-jsx "^5.3.2" - acorn-jsx-walk "^2.0.0" - acorn-loose "^8.4.0" - acorn-walk "^8.3.4" - ajv "^8.17.1" - commander "^12.1.0" - enhanced-resolve "^5.17.1" - ignore "^6.0.2" - interpret "^3.1.1" - is-installed-globally "^1.0.0" - json5 "^2.2.3" - memoize "^10.0.0" - picocolors "^1.1.1" - picomatch "^4.0.2" - prompts "^2.4.2" - rechoir "^0.8.0" - safe-regex "^2.1.1" - semver "^7.6.3" - teamcity-service-messages "^0.1.14" - tsconfig-paths-webpack-plugin "^4.2.0" - watskeburt "^4.1.1" - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -detect-libc@^2.0.0: - version "2.0.3" - resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz" - integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -docker-modem@^5.0.3: - version "5.0.3" - resolved "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz" - integrity sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg== - dependencies: - debug "^4.1.1" - readable-stream "^3.5.0" - split-ca "^1.0.1" - ssh2 "^1.15.0" - -dockerode@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz" - integrity sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w== - dependencies: - "@balena/dockerignore" "^1.0.2" - docker-modem "^5.0.3" - tar-fs "~2.0.1" - -doctrine@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dunder-proto@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz" - integrity sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-errors "^1.3.0" - gopd "^1.2.0" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -emoji-regex@^10.3.0: - version "10.4.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz" - integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -enabled@2.0.x: - version "2.0.0" - resolved "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz" - integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -encodeurl@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" - integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== - -encoding@^0.1.0, encoding@^0.1.12: - version "0.1.13" - resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" - integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== - dependencies: - iconv-lite "^0.6.2" - -end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enhanced-resolve@^5.17.1, enhanced-resolve@^5.7.0: - version "5.17.1" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz" - integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - -err-code@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz" - integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== - -es-define-property@^1.0.0, es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -esbuild@~0.23.0: - version "0.23.1" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz" - integrity sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg== - optionalDependencies: - "@esbuild/aix-ppc64" "0.23.1" - "@esbuild/android-arm" "0.23.1" - "@esbuild/android-arm64" "0.23.1" - "@esbuild/android-x64" "0.23.1" - "@esbuild/darwin-arm64" "0.23.1" - "@esbuild/darwin-x64" "0.23.1" - "@esbuild/freebsd-arm64" "0.23.1" - "@esbuild/freebsd-x64" "0.23.1" - "@esbuild/linux-arm" "0.23.1" - "@esbuild/linux-arm64" "0.23.1" - "@esbuild/linux-ia32" "0.23.1" - "@esbuild/linux-loong64" "0.23.1" - "@esbuild/linux-mips64el" "0.23.1" - "@esbuild/linux-ppc64" "0.23.1" - "@esbuild/linux-riscv64" "0.23.1" - "@esbuild/linux-s390x" "0.23.1" - "@esbuild/linux-x64" "0.23.1" - "@esbuild/netbsd-x64" "0.23.1" - "@esbuild/openbsd-arm64" "0.23.1" - "@esbuild/openbsd-x64" "0.23.1" - "@esbuild/sunos-x64" "0.23.1" - "@esbuild/win32-arm64" "0.23.1" - "@esbuild/win32-ia32" "0.23.1" - "@esbuild/win32-x64" "0.23.1" - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -expand-template@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz" - integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== - -express-rate-limit@^7.4.1: - version "7.4.1" - resolved "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz" - integrity sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg== - -express@^4.21.1, "express@>=4.0.0 || >=5.0.0-beta", "express@4 || 5 || ^5.0.0-beta.1": - version "4.21.2" - resolved "https://registry.npmjs.org/express/-/express-4.21.2.tgz" - integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.3" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.7.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~2.0.0" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.3.1" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.3" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.12" - proxy-addr "~2.0.7" - qs "6.13.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.19.0" - serve-static "1.16.2" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-uri@^3.0.1: - version "3.0.3" - resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz" - integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== - -fecha@^4.2.0: - version "4.2.3" - resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz" - integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== - -fetch-blob@^3.1.2, fetch-blob@^3.1.4: - version "3.2.0" - resolved "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz" - integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== - dependencies: - node-domexception "^1.0.0" - web-streams-polyfill "^3.0.3" - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz" - integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== - dependencies: - debug "2.6.9" - encodeurl "~2.0.0" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" - -fn.name@1.x.x: - version "1.1.0" - resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz" - integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== - -form-data@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz" - integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -formdata-polyfill@^4.0.10: - version "4.0.10" - resolved "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz" - integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== - dependencies: - fetch-blob "^3.1.2" - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs-minipass@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -gauge@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz" - integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== - dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.2" - console-control-strings "^1.0.0" - has-unicode "^2.0.1" - object-assign "^4.1.1" - signal-exit "^3.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.2" - -gauge@^4.0.3: - version "4.0.4" - resolved "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz" - integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== - dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.3" - console-control-strings "^1.1.0" - has-unicode "^2.0.1" - signal-exit "^3.0.7" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.5" - -get-east-asian-width@^1.0.0: - version "1.3.0" - resolved "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz" - integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== - -get-intrinsic@^1.2.4: - version "1.2.5" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz" - integrity sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg== - dependencies: - call-bind-apply-helpers "^1.0.0" - dunder-proto "^1.0.0" - es-define-property "^1.0.1" - es-errors "^1.3.0" - function-bind "^1.1.2" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - -get-tsconfig@^4.7.5: - version "4.8.1" - resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz" - integrity sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg== - dependencies: - resolve-pkg-maps "^1.0.0" - -github-from-package@0.0.0: - version "0.0.0" - resolved "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz" - integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== - -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob@^7.1.3, glob@^7.1.4: - version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@7.1.6: - version "7.1.6" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-directory@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz" - integrity sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q== - dependencies: - ini "4.1.1" - -gopd@^1.0.1, gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -graceful-fs@^4.2.4, graceful-fs@^4.2.6: - version "4.2.11" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" - integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== - -hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -http-cache-semantics@^4.1.0: - version "4.1.1" - resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz" - integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http-proxy-agent@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== - dependencies: - "@tootallnate/once" "1" - agent-base "6" - debug "4" - -https-proxy-agent@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - -https@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/https/-/https-1.0.0.tgz" - integrity sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg== - -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz" - integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== - dependencies: - ms "^2.0.0" - -iconv-lite@^0.6.2: - version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore-by-default@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz" - integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== - -ignore@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz" - integrity sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A== - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -infer-owner@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz" - integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@^2.0.3, inherits@^2.0.4, inherits@2, inherits@2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ini@~1.3.0: - version "1.3.8" - resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -ini@4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz" - integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== - -interpret@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz" - integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== - -ip-address@^9.0.5: - version "9.0.5" - resolved "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz" - integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== - dependencies: - jsbn "1.1.0" - sprintf-js "^1.1.3" - -ipaddr.js@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz" - integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-core-module@^2.13.0: - version "2.15.1" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz" - integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== - dependencies: - hasown "^2.0.2" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^3.0.0: - 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== - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-installed-globally@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz" - integrity sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ== - dependencies: - global-directory "^4.0.1" - is-path-inside "^4.0.0" - -is-interactive@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz" - integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ== - -is-lambda@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz" - integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-path-inside@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz" - integrity sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA== - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-unicode-supported@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz" - integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== - -is-unicode-supported@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz" - integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsbn@1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz" - integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json5@^2.2.2, json5@^2.2.3: - version "2.2.3" - resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - -kuler@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz" - integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== - -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" - integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== - -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz" - integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== - -lodash.mergewith@^4.6.2: - version "4.6.2" - resolved "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz" - integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== - -log-symbols@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz" - integrity sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw== - dependencies: - chalk "^5.3.0" - is-unicode-supported "^1.3.0" - -logform@^2.7.0: - version "2.7.0" - resolved "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz" - integrity sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ== - dependencies: - "@colors/colors" "1.6.0" - "@types/triple-beam" "^1.3.2" - fecha "^4.2.0" - ms "^2.1.1" - safe-stable-stringify "^2.3.1" - triple-beam "^1.3.0" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -make-dir@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -make-fetch-happen@^9.1.0: - version "9.1.0" - resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz" - integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== - dependencies: - agentkeepalive "^4.1.3" - cacache "^15.2.0" - http-cache-semantics "^4.1.0" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^6.0.0" - minipass "^3.1.3" - minipass-collect "^1.0.2" - minipass-fetch "^1.3.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.2" - promise-retry "^2.0.1" - socks-proxy-agent "^6.0.0" - ssri "^8.0.0" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -memoize@^10.0.0: - version "10.0.0" - resolved "https://registry.npmjs.org/memoize/-/memoize-10.0.0.tgz" - integrity sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA== - dependencies: - mimic-function "^5.0.0" - -merge-descriptors@1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" - integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mimic-function@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz" - integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== - -mimic-response@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz" - integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== - -minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -minipass-collect@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz" - integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== - dependencies: - minipass "^3.0.0" - -minipass-fetch@^1.3.2: - version "1.4.1" - resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz" - integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== - dependencies: - minipass "^3.1.0" - minipass-sized "^1.0.3" - minizlib "^2.0.0" - optionalDependencies: - encoding "^0.1.12" - -minipass-flush@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz" - integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== - dependencies: - minipass "^3.0.0" - -minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz" - integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== - dependencies: - minipass "^3.0.0" - -minipass-sized@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz" - integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== - dependencies: - minipass "^3.0.0" - -minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: - version "3.3.6" - resolved "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz" - integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== - dependencies: - yallist "^4.0.0" - -minipass@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz" - integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== - -minizlib@^2.0.0, minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - -mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: - version "0.5.3" - resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== - -mkdirp@^1.0.3, mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -ms@^2.0.0, ms@^2.1.1, ms@^2.1.3, ms@2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -nan@^2.19.0, nan@^2.20.0: - version "2.22.0" - resolved "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz" - integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw== - -napi-build-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz" - integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== - -negotiator@^0.6.2, negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -node-abi@^3.3.0: - version "3.71.0" - resolved "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz" - integrity sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw== - dependencies: - semver "^7.3.5" - -node-addon-api@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz" - integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== - -node-addon-api@^7.0.0: - version "7.1.1" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz" - integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== - -node-domexception@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz" - integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== - -node-fetch@^2.6.7: - version "2.7.0" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - -node-fetch@^3.3.2: - version "3.3.2" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz" - integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== - dependencies: - data-uri-to-buffer "^4.0.0" - fetch-blob "^3.1.4" - formdata-polyfill "^4.0.10" - -node-gyp@8.x: - version "8.4.1" - resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz" - integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.6" - make-fetch-happen "^9.1.0" - nopt "^5.0.0" - npmlog "^6.0.0" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.2" - which "^2.0.2" - -nodemailer@^6.9.16: - version "6.9.16" - resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz" - integrity sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ== - -nodemon@^3.1.7: - version "3.1.7" - resolved "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz" - integrity sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ== - dependencies: - chokidar "^3.5.2" - debug "^4" - ignore-by-default "^1.0.1" - minimatch "^3.1.2" - pstree.remy "^1.1.8" - semver "^7.5.3" - simple-update-notifier "^2.0.0" - supports-color "^5.5.0" - touch "^3.1.0" - undefsafe "^2.0.5" - -nopt@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz" - integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== - dependencies: - abbrev "1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -npmlog@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz" - integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== - dependencies: - are-we-there-yet "^2.0.0" - console-control-strings "^1.1.0" - gauge "^3.0.0" - set-blocking "^2.0.0" - -npmlog@^6.0.0: - version "6.0.2" - resolved "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz" - integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== - dependencies: - are-we-there-yet "^3.0.0" - console-control-strings "^1.1.0" - gauge "^4.0.3" - set-blocking "^2.0.0" - -object-assign@^4, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.13.1: - version "1.13.3" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz" - integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== - -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -one-time@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz" - integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== - dependencies: - fn.name "1.x.x" - -onetime@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz" - integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== - dependencies: - mimic-function "^5.0.0" - -openapi-types@>=7: - version "12.1.3" - resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz" - integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== - -ora@^8.1.1: - version "8.1.1" - resolved "https://registry.npmjs.org/ora/-/ora-8.1.1.tgz" - integrity sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw== - dependencies: - chalk "^5.3.0" - cli-cursor "^5.0.0" - cli-spinners "^2.9.2" - is-interactive "^2.0.0" - is-unicode-supported "^2.0.0" - log-symbols "^6.0.0" - stdin-discarder "^0.2.2" - string-width "^7.2.0" - strip-ansi "^7.1.0" - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-to-regexp@0.1.12: - version "0.1.12" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz" - integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== - -picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -picomatch@^2.0.4: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -picomatch@^2.2.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -picomatch@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz" - integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== - -playwright-core@1.49.0: - version "1.49.0" - resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz" - integrity sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA== - -playwright@1.49.0: - version "1.49.0" - resolved "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz" - integrity sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A== - dependencies: - playwright-core "1.49.0" - optionalDependencies: - fsevents "2.3.2" - -prebuild-install@^7.1.1: - version "7.1.2" - resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz" - integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ== - dependencies: - detect-libc "^2.0.0" - expand-template "^2.0.3" - github-from-package "0.0.0" - minimist "^1.2.3" - mkdirp-classic "^0.5.3" - napi-build-utils "^1.0.1" - node-abi "^3.3.0" - pump "^3.0.0" - rc "^1.2.7" - simple-get "^4.0.0" - tar-fs "^2.0.0" - tunnel-agent "^0.6.0" - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz" - integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== - -promise-retry@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz" - integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== - dependencies: - err-code "^2.0.2" - retry "^0.12.0" - -prompts@^2.4.2: - version "2.4.2" - resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" - integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - -proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -pstree.remy@^1.1.8: - version "1.1.8" - resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz" - integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== - -pump@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz" - integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -qs@6.13.0: - version "6.13.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz" - integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== - dependencies: - side-channel "^1.0.6" - -range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz" - integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -rc@^1.2.7: - version "1.2.8" - resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2: - version "3.6.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz" - integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -rechoir@^0.8.0: - version "0.8.0" - resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz" - integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== - dependencies: - resolve "^1.20.0" - -regexp-tree@~0.1.1: - version "0.1.27" - resolved "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz" - integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -resolve-pkg-maps@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" - integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== - -resolve@^1.20.0: - version "1.22.8" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -restore-cursor@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz" - integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== - dependencies: - onetime "^7.0.0" - signal-exit "^4.1.0" - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" - integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -safe-buffer@^5.0.1, safe-buffer@~5.2.0, safe-buffer@5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-regex@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz" - integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A== - dependencies: - regexp-tree "~0.1.1" - -safe-stable-stringify@^2.3.1: - version "2.5.0" - resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz" - integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -semver@^6.0.0: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.3.5, semver@^7.5.3, semver@^7.6.3: - version "7.6.3" - resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - -send@0.19.0: - version "0.19.0" - resolved "https://registry.npmjs.org/send/-/send-0.19.0.tgz" - integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - -serve-static@1.16.2: - version "1.16.2" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz" - integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== - dependencies: - encodeurl "~2.0.0" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.19.0" - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" - integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - -set-function-length@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -side-channel@^1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" - -signal-exit@^3.0.0, signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -signal-exit@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - -simple-concat@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz" - integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== - -simple-get@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz" - integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== - dependencies: - decompress-response "^6.0.0" - once "^1.3.1" - simple-concat "^1.0.0" - -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz" - integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== - dependencies: - is-arrayish "^0.3.1" - -simple-update-notifier@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz" - integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== - dependencies: - semver "^7.5.3" - -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - -smart-buffer@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" - integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== - -socks-proxy-agent@^6.0.0: - version "6.2.1" - resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz" - integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== - dependencies: - agent-base "^6.0.2" - debug "^4.3.3" - socks "^2.6.2" - -socks@^2.6.2: - version "2.8.3" - resolved "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz" - integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== - dependencies: - ip-address "^9.0.5" - smart-buffer "^4.2.0" - -split-ca@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz" - integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ== - -sprintf-js@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz" - integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== - -sqlite3@^5.1.7: - version "5.1.7" - resolved "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz" - integrity sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog== - dependencies: - bindings "^1.5.0" - node-addon-api "^7.0.0" - prebuild-install "^7.1.1" - tar "^6.1.11" - optionalDependencies: - node-gyp "8.x" - -ssh2@^1.15.0: - version "1.16.0" - resolved "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz" - integrity sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg== - dependencies: - asn1 "^0.2.6" - bcrypt-pbkdf "^1.0.2" - optionalDependencies: - cpu-features "~0.0.10" - nan "^2.20.0" - -ssri@^8.0.0, ssri@^8.0.1: - version "8.0.1" - resolved "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz" - integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== - dependencies: - minipass "^3.1.1" - -stack-trace@0.0.x: - version "0.0.10" - resolved "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz" - integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -stdin-discarder@^0.2.2: - version "0.2.2" - resolved "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz" - integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ== - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^7.2.0: - version "7.2.0" - resolved "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz" - integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== - dependencies: - emoji-regex "^10.3.0" - get-east-asian-width "^1.0.0" - strip-ansi "^7.1.0" - -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.0.1" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" - integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" - integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== - -supports-color@^5.5.0: - version "5.5.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -swagger-jsdoc@^6.2.8: - version "6.2.8" - resolved "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz" - integrity sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ== - dependencies: - commander "6.2.0" - doctrine "3.0.0" - glob "7.1.6" - lodash.mergewith "^4.6.2" - swagger-parser "^10.0.3" - yaml "2.0.0-1" - -swagger-parser@^10.0.3: - version "10.0.3" - resolved "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz" - integrity sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg== - dependencies: - "@apidevtools/swagger-parser" "10.0.3" - -swagger-ui-dist@>=5.0.0: - version "5.18.2" - resolved "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz" - integrity sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw== - dependencies: - "@scarf/scarf" "=1.4.0" - -swagger-ui-express@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz" - integrity sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA== - dependencies: - swagger-ui-dist ">=5.0.0" - -tapable@^2.2.0, tapable@^2.2.1: - version "2.2.1" - resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - -tar-fs@^2.0.0, tar-fs@~2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz" - integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.0.0" - -tar-stream@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: - version "6.2.1" - resolved "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz" - integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^5.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -teamcity-service-messages@^0.1.14: - version "0.1.14" - resolved "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz" - integrity sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w== - -text-hex@1.0.x: - version "1.0.0" - resolved "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz" - integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -touch@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz" - integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -triple-beam@^1.3.0: - version "1.4.1" - resolved "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz" - integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== - -ts-node@^10.9.2: - version "10.9.2" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" - integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - -tsconfig-paths-webpack-plugin@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz" - integrity sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA== - dependencies: - chalk "^4.1.0" - enhanced-resolve "^5.7.0" - tapable "^2.2.1" - tsconfig-paths "^4.1.2" - -tsconfig-paths@^4.1.2: - version "4.2.0" - resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz" - integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== - dependencies: - json5 "^2.2.2" - minimist "^1.2.6" - strip-bom "^3.0.0" - -tsx@^4.19.2: - version "4.19.2" - resolved "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz" - integrity sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g== - dependencies: - esbuild "~0.23.0" - get-tsconfig "^4.7.5" - optionalDependencies: - fsevents "~2.3.3" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" - integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3: - version "0.14.5" - resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" - integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== - -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typescript@>=2.7: - version "5.7.2" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz" - integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== - -uglify-js@^3.19.3: - version "3.19.3" - resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz" - integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== - -undefsafe@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz" - integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== - -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - -undici-types@~6.20.0: - version "6.20.0" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz" - integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== - -unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== - dependencies: - unique-slug "^2.0.0" - -unique-slug@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz" - integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== - dependencies: - imurmurhash "^0.1.4" - -unpipe@~1.0.0, unpipe@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - -validator@^13.7.0: - version "13.12.0" - resolved "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz" - integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== - -vary@^1, vary@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== - -watskeburt@^4.1.1: - version "4.2.2" - resolved "https://registry.npmjs.org/watskeburt/-/watskeburt-4.2.2.tgz" - integrity sha512-AOCg1UYxWpiHW1tUwqpJau8vzarZYTtzl2uu99UptBmbzx6kOzCGMfRLF6KIRX4PYekmryn89MzxlRNkL66YyA== - -web-streams-polyfill@^3.0.3: - version "3.3.3" - resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz" - integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.2, wide-align@^1.1.5: - version "1.1.5" - resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz" - integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== - dependencies: - string-width "^1.0.2 || 2 || 3 || 4" - -winston-transport@^4.9.0: - version "4.9.0" - resolved "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz" - integrity sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A== - dependencies: - logform "^2.7.0" - readable-stream "^3.6.2" - triple-beam "^1.3.0" - -winston@^3.15.0: - version "3.17.0" - resolved "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz" - integrity sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw== - dependencies: - "@colors/colors" "^1.6.0" - "@dabh/diagnostics" "^2.0.2" - async "^3.2.3" - is-stream "^2.0.0" - logform "^2.7.0" - one-time "^1.0.0" - readable-stream "^3.4.0" - safe-stable-stringify "^2.3.1" - stack-trace "0.0.x" - triple-beam "^1.3.0" - winston-transport "^4.9.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@2.0.0-1: - version "2.0.0-1" - resolved "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz" - integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== - -yn@3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - -z-schema@^5.0.1: - version "5.0.5" - resolved "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz" - integrity sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q== - dependencies: - lodash.get "^4.4.2" - lodash.isequal "^4.5.0" - validator "^13.7.0" - optionalDependencies: - commander "^9.4.1" From ff34dff392c284cd6339d9809acbcbb68411df76 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 10:58:07 +0100 Subject: [PATCH 072/369] Fix: Added missing varaible.json file for workflow --- .github/workflows/validation.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index d46610bc..395569fe 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -18,6 +18,9 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts + - name: Create varaibles.json + run: npm run local-env-file + - name: Run prettier run: npm run prettier From edaab084a2fc9644354c8d6dc056832c6fe0665e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 11:02:27 +0100 Subject: [PATCH 073/369] Chore: Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e24b1497..4e6daf38 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ _Pipelines:_
[![Docker Image CI](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml/badge.svg?branch=main)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml)
[![Build dockstatapi:nightly](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml)
+[![Tests](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. With this new release a couple of extra features (compared to v1) are going to be available. From d62abc09883dbcba17d1a6d1339bc9724ef8da1e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 11:12:56 +0100 Subject: [PATCH 074/369] Fix: Added "..." to workflow name --- .github/workflows/anchore.yml | 2 +- .github/workflows/build-dev.yaml | 2 +- .github/workflows/build-image.yml | 2 +- .github/workflows/build-test.yaml | 2 +- .github/workflows/cloc.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml index 2725a7cc..c09b20e9 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yml @@ -1,4 +1,4 @@ -name: Anchore Grype vulnerability scan +name: "Anchore Grype vulnerability scan" on: [push] diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index f21ab4ac..62e0da98 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -1,4 +1,4 @@ -name: Build dockstatapi:nightly +name: "Build dockstatapi:nightly" on: push: diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 17933f97..9e382a29 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -1,4 +1,4 @@ -name: Build dockstatapi:latest +name: "Build dockstatapi:latest" on: release: diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 2f2322f5..631af23e 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -1,4 +1,4 @@ -name: Build test docker image +name: "Build test docker image" on: [push] diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml index d29afa4a..004f51b6 100644 --- a/.github/workflows/cloc.yaml +++ b/.github/workflows/cloc.yaml @@ -1,4 +1,4 @@ -name: Count Lines of Code +name: "Count Lines of Code" permissions: issues: write From 569044d224c4f98b2d1c05799f4f125750053806 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 16:11:10 +0100 Subject: [PATCH 075/369] Feat: Add better workflow logic --- .github/workflows/{anchore.yml => anchore.yaml} | 3 ++- .github/workflows/{build-image.yml => build-image.yaml} | 0 .github/workflows/build-test.yaml | 3 ++- .github/workflows/{codeql.yml => codeql.yaml} | 7 +------ .../workflows/{remove-stale.yml => remove-stale.yaml} | 0 .github/workflows/{validation.yml => validation.yaml} | 9 +++++++++ 6 files changed, 14 insertions(+), 8 deletions(-) rename .github/workflows/{anchore.yml => anchore.yaml} (97%) rename .github/workflows/{build-image.yml => build-image.yaml} (100%) rename .github/workflows/{codeql.yml => codeql.yaml} (91%) rename .github/workflows/{remove-stale.yml => remove-stale.yaml} (100%) rename .github/workflows/{validation.yml => validation.yaml} (75%) diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yaml similarity index 97% rename from .github/workflows/anchore.yml rename to .github/workflows/anchore.yaml index c09b20e9..290694c3 100644 --- a/.github/workflows/anchore.yml +++ b/.github/workflows/anchore.yaml @@ -1,6 +1,7 @@ name: "Anchore Grype vulnerability scan" -on: [push] +on: + workflow_call: permissions: contents: read diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yaml similarity index 100% rename from .github/workflows/build-image.yml rename to .github/workflows/build-image.yaml diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 631af23e..a87c5d8a 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -1,6 +1,7 @@ name: "Build test docker image" -on: [push] +on: + workflow_call: permissions: packages: write diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yaml similarity index 91% rename from .github/workflows/codeql.yml rename to .github/workflows/codeql.yaml index 081205c6..d0eec671 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yaml @@ -1,12 +1,7 @@ name: "CodeQL Advanced" on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - schedule: - - cron: "32 1 * * 5" + workflow_call: jobs: codeql: diff --git a/.github/workflows/remove-stale.yml b/.github/workflows/remove-stale.yaml similarity index 100% rename from .github/workflows/remove-stale.yml rename to .github/workflows/remove-stale.yaml diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yaml similarity index 75% rename from .github/workflows/validation.yml rename to .github/workflows/validation.yaml index 395569fe..05f2377f 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yaml @@ -32,3 +32,12 @@ jobs: - name: Audit packages run: npm audit --audit-level=high + + - name: Run Anchore + uses: ./.github/workflows/anchore.yml + + - name: Run CodeQL + uses: ./.github/workflows/codeql.yml + + - name: Test build + uses: ./.github/workflows/build-test.yaml From 8183c6a648bd1712d1caf924b85d8923b3072eca Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 16:12:33 +0100 Subject: [PATCH 076/369] Fix: Renamed files => adjusted in code --- .github/workflows/validation.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 05f2377f..eec6609f 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -34,10 +34,10 @@ jobs: run: npm audit --audit-level=high - name: Run Anchore - uses: ./.github/workflows/anchore.yml + uses: ./.github/workflows/anchore.yaml - name: Run CodeQL - uses: ./.github/workflows/codeql.yml + uses: ./.github/workflows/codeql.yaml - name: Test build uses: ./.github/workflows/build-test.yaml From c646f0e349866a252d681894c785763a08adc1ac Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 16:23:18 +0100 Subject: [PATCH 077/369] Fix: Workflows depending on each other (test) --- .github/workflows/anchore.yaml | 10 +++++++--- .github/workflows/build-test.yaml | 7 ++++++- .github/workflows/codeql.yaml | 10 +++++++--- .github/workflows/validation.yaml | 6 ------ 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.github/workflows/anchore.yaml b/.github/workflows/anchore.yaml index 290694c3..24ce2186 100644 --- a/.github/workflows/anchore.yaml +++ b/.github/workflows/anchore.yaml @@ -1,7 +1,11 @@ -name: "Anchore Grype vulnerability scan" +name: "Anchore Grype Vulnerability Scan" on: - workflow_call: + workflow_run: + workflows: + - "Run all tests" # Replace with the actual name of the preceding workflow + types: + - completed permissions: contents: read @@ -16,7 +20,7 @@ jobs: - name: Download Grype run: | curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/checkout@v4 - name: Build the Container image run: docker build . --file Dockerfile --tag localbuild/testimage:latest - name: Run Grype test diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index a87c5d8a..5113169e 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -1,7 +1,12 @@ name: "Build test docker image" on: - workflow_call: + workflow_run: + workflows: + - "Anchore Grype Vulnerability Scan" + - "CodeQL Advanced" + types: + - completed permissions: packages: write diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index d0eec671..7ba4587a 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -1,12 +1,16 @@ name: "CodeQL Advanced" on: - workflow_call: + workflow_run: + workflows: + - "Anchore Grype Vulnerability Scan" + types: + - completed jobs: codeql: name: Analyze TypeScript - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest permissions: security-events: write packages: read @@ -44,4 +48,4 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: - category: "/language:${{matrix.language}}" + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index eec6609f..3c6f2cec 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -33,11 +33,5 @@ jobs: - name: Audit packages run: npm audit --audit-level=high - - name: Run Anchore - uses: ./.github/workflows/anchore.yaml - - - name: Run CodeQL - uses: ./.github/workflows/codeql.yaml - - name: Test build uses: ./.github/workflows/build-test.yaml From 19a1d242060aec391aa92e863cd404add8c7c178 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 16:24:55 +0100 Subject: [PATCH 078/369] Fix: Remove old 'residue' of workflow files --- .github/workflows/validation.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 3c6f2cec..395569fe 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -32,6 +32,3 @@ jobs: - name: Audit packages run: npm audit --audit-level=high - - - name: Test build - uses: ./.github/workflows/build-test.yaml From 16b0aa90019c245666524b6780d6d3a2b7736862 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 16:33:42 +0100 Subject: [PATCH 079/369] Fix: Adjust workflows --- .github/workflows/anchore.yaml | 8 +++ .github/workflows/build-test.yaml | 13 +++++ .github/workflows/codeql.yaml | 3 ++ CREDITS.md | 73 ++++++++++++-------------- package.json | 2 +- src/controllers/containerController.ts | 8 ++- 6 files changed, 62 insertions(+), 45 deletions(-) diff --git a/.github/workflows/anchore.yaml b/.github/workflows/anchore.yaml index 24ce2186..436ba6bd 100644 --- a/.github/workflows/anchore.yaml +++ b/.github/workflows/anchore.yaml @@ -17,15 +17,23 @@ jobs: steps: - name: Set up Grype installation path run: echo "$HOME/bin" >> $GITHUB_PATH + - name: Download Grype run: | curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin + - uses: actions/checkout@v4 + - name: Build the Container image run: docker build . --file Dockerfile --tag localbuild/testimage:latest + - name: Run Grype test run: grype -o sarif localbuild/testimage:latest > results.sarif + - name: Upload Anchore scan SARIF report uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ./results.sarif + + - name: Set Marker for Workflow Completion + run: echo "anchore_complete=true" >> $GITHUB_ENV diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 5113169e..a0f6db26 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -16,6 +16,19 @@ jobs: build-test: runs-on: ubuntu-latest steps: + - name: Check workflow dependencies + run: | + for i in {1..10}; do + if [[ "${{ env.anchore_complete }}" == "true" && "${{ env.codeql_complete }}" == "true" ]]; then + echo "All workflows complete!" + exit 0 + fi + echo "Dependencies not yet complete. Retrying in 60 seconds..." + sleep 60 + done + echo "Dependencies not met within the timeout period. Exiting." + exit 1 + - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 7ba4587a..4e2f5f92 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -49,3 +49,6 @@ jobs: uses: github/codeql-action/analyze@v3 with: category: "/language:${{ matrix.language }}" + + - name: Set Marker for Workflow Completion + run: echo "codeql_complete=true" >> $GITHUB_ENV diff --git a/CREDITS.md b/CREDITS.md index be34b479..050b430b 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -4,53 +4,48 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) ### License: (MIT AND CC-BY-3.0) -| Name | Repository | Publisher | -|------|-------------|-----------| +| Name | Repository | Publisher | +| ----------------- | -------------------------------------------- | -------------------- | | spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | - ### License: Apache-2.0 -| Name | Repository | Publisher | -|------|-------------|-----------| -| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | -| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | -| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | -| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | -| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | -| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | -| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | -| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | -| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | - +| Name | Repository | Publisher | +| ------------------------------------ | ------------------------------------------------------------- | --------------------- | +| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | +| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | +| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | +| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | +| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | +| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | +| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | +| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | +| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | +| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | ### License: CC-BY-3.0 -| Name | Repository | Publisher | -|------|-------------|-----------| +| Name | Repository | Publisher | +| --------------------- | -------------------------------------------------- | -------------------- | | spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | - ### License: Python-2.0 -| Name | Repository | Publisher | -|------|-------------|-----------| -| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | - - +| Name | Repository | Publisher | +| -------------- | ---------------------------------- | --------- | +| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | diff --git a/package.json b/package.json index 9acd9525..90422a28 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "docker:full": "docker compose up -d && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", "docker:build": "docker build . -t \"dockstatapi:local\" -f ./Dockerfile-dev && docker compose up -d", "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", - "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write && npx prettier -c ./.github/workflows/*.{yaml,yml} --parser yaml --write && npx prettier -c ./**/*.md --parser markdown --write && npx prettier -c ./**/*.json --parser json --write", + "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write && npx prettier -c ./.github/workflows/*.yaml --parser yaml --write && npx prettier -c ./**/*.md --parser markdown --write && npx prettier -c ./**/*.json --parser json --write", "lint": "npx eslint", "lint:fix": "npx eslint --fix", "license": "bash ./src/misc/credits.sh", diff --git a/src/controllers/containerController.ts b/src/controllers/containerController.ts index 8d3bef30..ef1c8cef 100644 --- a/src/controllers/containerController.ts +++ b/src/controllers/containerController.ts @@ -40,11 +40,9 @@ const getContainerStats = async ( logger.error( `Error fetching stats for container: ${containerID} from host: ${containerHost} - ${(error as Error).message}`, ); - res - .status(500) - .json({ - error: `Error fetching container stats: ${(error as Error).message}`, - }); + res.status(500).json({ + error: `Error fetching container stats: ${(error as Error).message}`, + }); } }; From 2766aa626e0f8243e89a508816b2e9b9c13fb687 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 16:49:11 +0100 Subject: [PATCH 080/369] Fix: Adjust workflows (hopefully) --- .github/workflows/anchore.yaml | 6 +----- .github/workflows/build-test.yaml | 20 +------------------- .github/workflows/codeql.yaml | 6 +----- .github/workflows/validation.yaml | 17 +++++++++++++++++ 4 files changed, 20 insertions(+), 29 deletions(-) diff --git a/.github/workflows/anchore.yaml b/.github/workflows/anchore.yaml index 436ba6bd..7aa91629 100644 --- a/.github/workflows/anchore.yaml +++ b/.github/workflows/anchore.yaml @@ -1,11 +1,7 @@ name: "Anchore Grype Vulnerability Scan" on: - workflow_run: - workflows: - - "Run all tests" # Replace with the actual name of the preceding workflow - types: - - completed + workflow_call: permissions: contents: read diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index a0f6db26..a87c5d8a 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -1,12 +1,7 @@ name: "Build test docker image" on: - workflow_run: - workflows: - - "Anchore Grype Vulnerability Scan" - - "CodeQL Advanced" - types: - - completed + workflow_call: permissions: packages: write @@ -16,19 +11,6 @@ jobs: build-test: runs-on: ubuntu-latest steps: - - name: Check workflow dependencies - run: | - for i in {1..10}; do - if [[ "${{ env.anchore_complete }}" == "true" && "${{ env.codeql_complete }}" == "true" ]]; then - echo "All workflows complete!" - exit 0 - fi - echo "Dependencies not yet complete. Retrying in 60 seconds..." - sleep 60 - done - echo "Dependencies not met within the timeout period. Exiting." - exit 1 - - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 4e2f5f92..c187b095 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -1,11 +1,7 @@ name: "CodeQL Advanced" on: - workflow_run: - workflows: - - "Anchore Grype Vulnerability Scan" - types: - - completed + workflow_call: jobs: codeql: diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 395569fe..1918efdc 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -32,3 +32,20 @@ jobs: - name: Audit packages run: npm audit --audit-level=high + + code-testing: + needs: validation + runs-on: ubuntu-latest + steps: + - name: CodeQL + uses: ./.github/workflows/codeql.yaml + + - name: Anchore + uses: ./.github/workflows/anchore.yaml + + test-building: + needs: code-testing + runs-on: ubuntu-latest + steps: + - name: Test build + uses: ./.github/workflows/build-test.yaml From 406fe0cfd7990a807e5fec8e9bba16a27c387205 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 16:52:21 +0100 Subject: [PATCH 081/369] Fix: More adjustments --- .github/workflows/validation.yaml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 1918efdc..9a6387ef 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -33,18 +33,28 @@ jobs: - name: Audit packages run: npm audit --audit-level=high - code-testing: + CodeQL: needs: validation runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v4 + - name: CodeQL uses: ./.github/workflows/codeql.yaml + Anchore: + needs: validation + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Anchore uses: ./.github/workflows/anchore.yaml test-building: - needs: code-testing + needs: [CodeQL, Anchore] runs-on: ubuntu-latest steps: - name: Test build From 25696d8f44d726e495d31a256ed1ef41ee98548d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 17:08:37 +0100 Subject: [PATCH 082/369] Fix: Move workflow files around --- .github/workflows/anchore.yaml | 35 ---------- .github/workflows/build-test.yaml | 45 ------------ .github/workflows/codeql.yaml | 50 -------------- .github/workflows/validation.yaml | 109 +++++++++++++++++++++++++++--- 4 files changed, 100 insertions(+), 139 deletions(-) delete mode 100644 .github/workflows/anchore.yaml delete mode 100644 .github/workflows/codeql.yaml diff --git a/.github/workflows/anchore.yaml b/.github/workflows/anchore.yaml deleted file mode 100644 index 7aa91629..00000000 --- a/.github/workflows/anchore.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: "Anchore Grype Vulnerability Scan" - -on: - workflow_call: - -permissions: - contents: read - security-events: write - -jobs: - anchore: - runs-on: ubuntu-latest - steps: - - name: Set up Grype installation path - run: echo "$HOME/bin" >> $GITHUB_PATH - - - name: Download Grype - run: | - curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin - - - uses: actions/checkout@v4 - - - name: Build the Container image - run: docker build . --file Dockerfile --tag localbuild/testimage:latest - - - name: Run Grype test - run: grype -o sarif localbuild/testimage:latest > results.sarif - - - name: Upload Anchore scan SARIF report - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: ./results.sarif - - - name: Set Marker for Workflow Completion - run: echo "anchore_complete=true" >> $GITHUB_ENV diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index a87c5d8a..5ea74362 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -10,48 +10,3 @@ permissions: jobs: build-test: runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Set up Node.js using nvm - - name: Set up Node.js version from .nvmrc - run: | - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - nvm install - nvm use - node -v - npm -v - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Github Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate Docker tags - uses: docker/metadata-action@v5 - id: metadata - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,enable=true,priority=200,prefix=,suffix=,value=${{ github.sha }} - - - name: Build and Push Docker Images - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - push: false - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml deleted file mode 100644 index c187b095..00000000 --- a/.github/workflows/codeql.yaml +++ /dev/null @@ -1,50 +0,0 @@ -name: "CodeQL Advanced" - -on: - workflow_call: - -jobs: - codeql: - name: Analyze TypeScript - runs-on: ubuntu-latest - permissions: - security-events: write - packages: read - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - include: - - language: javascript-typescript - build-mode: none - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - queries: security-extended - - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{ matrix.language }}" - - - name: Set Marker for Workflow Completion - run: echo "codeql_complete=true" >> $GITHUB_ENV diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 9a6387ef..a0154c42 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -36,26 +36,117 @@ jobs: CodeQL: needs: validation runs-on: ubuntu-latest + name: Analyze TypeScript + permissions: + security-events: write + packages: read + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + steps: - - name: Checkout + - name: Checkout repository uses: actions/checkout@v4 - - name: CodeQL - uses: ./.github/workflows/codeql.yaml + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + queries: security-extended + + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" + + - name: Set Marker for Workflow Completion + run: echo "codeql_complete=true" >> $GITHUB_ENV Anchore: needs: validation runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 + - name: Set up Grype installation path + run: echo "$HOME/bin" >> $GITHUB_PATH + + - name: Download Grype + run: | + curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin + + - uses: actions/checkout@v4 + + - name: Build the Container image + run: docker build . --file Dockerfile --tag localbuild/testimage:latest - - name: Anchore - uses: ./.github/workflows/anchore.yaml + - name: Run Grype test + run: grype -o sarif localbuild/testimage:latest > results.sarif + + - name: Upload Anchore scan SARIF report + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ./results.sarif test-building: needs: [CodeQL, Anchore] runs-on: ubuntu-latest steps: - - name: Test build - uses: ./.github/workflows/build-test.yaml + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js version from .nvmrc + run: | + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + nvm install + nvm use + node -v + npm -v + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate Docker tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,enable=true,priority=200,prefix=,suffix=,value=${{ github.sha }} + + - name: Build and Push Docker Images + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + push: false + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From 7a4134fdefa731817cb5669b8331f6dd776f29a8 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 17:15:48 +0100 Subject: [PATCH 083/369] Fix: Fixing permissions --- .github/workflows/validation.yaml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index a0154c42..6bc15507 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -5,6 +5,11 @@ on: [push] jobs: validation: runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + actions: read + contents: read steps: - name: Checkout uses: actions/checkout@v4 @@ -76,12 +81,14 @@ jobs: with: category: "/language:${{ matrix.language }}" - - name: Set Marker for Workflow Completion - run: echo "codeql_complete=true" >> $GITHUB_ENV - Anchore: needs: validation runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + actions: read + contents: read steps: - name: Set up Grype installation path run: echo "$HOME/bin" >> $GITHUB_PATH @@ -106,6 +113,11 @@ jobs: test-building: needs: [CodeQL, Anchore] runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + actions: read + contents: read steps: - name: Checkout repository uses: actions/checkout@v4 From 61a49457e7e47f9ca45b00253209711adfece650 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 17:26:17 +0100 Subject: [PATCH 084/369] Feat: Added workflow naming --- .github/workflows/build-test.yaml | 12 ------------ .github/workflows/validation.yaml | 6 +++++- 2 files changed, 5 insertions(+), 13 deletions(-) delete mode 100644 .github/workflows/build-test.yaml diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml deleted file mode 100644 index 5ea74362..00000000 --- a/.github/workflows/build-test.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: "Build test docker image" - -on: - workflow_call: - -permissions: - packages: write - contents: read - -jobs: - build-test: - runs-on: ubuntu-latest diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 6bc15507..9d771ad8 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -5,6 +5,7 @@ on: [push] jobs: validation: runs-on: ubuntu-latest + name: Validation permissions: security-events: write packages: read @@ -66,7 +67,8 @@ jobs: build-mode: ${{ matrix.build-mode }} queries: security-extended - - if: matrix.build-mode == 'manual' + - name: Check build mode + if: matrix.build-mode == 'manual' shell: bash run: | echo 'If you are using a "manual" build mode for one or more of the' \ @@ -84,6 +86,7 @@ jobs: Anchore: needs: validation runs-on: ubuntu-latest + name: Anchore permissions: security-events: write packages: read @@ -113,6 +116,7 @@ jobs: test-building: needs: [CodeQL, Anchore] runs-on: ubuntu-latest + name: Test building permissions: security-events: write packages: read From ee37b8071e2393db94fa03208b25913ef82eeeff Mon Sep 17 00:00:00 2001 From: ItsNik Date: Mon, 30 Dec 2024 17:43:09 +0100 Subject: [PATCH 085/369] Fix: Preparing for yet another workflow change... --- .github/workflows/validation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 9d771ad8..5d6e8d38 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -114,7 +114,7 @@ jobs: sarif_file: ./results.sarif test-building: - needs: [CodeQL, Anchore] + needs: validation runs-on: ubuntu-latest name: Test building permissions: From a63c4b0258b9d62efc063578c2e455591bdfa677 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 31 Dec 2024 15:05:31 +0100 Subject: [PATCH 086/369] Feat: New workflow structure for dev build --- .github/workflows/build-dev.yaml | 47 ----------------------------- .github/workflows/validation.yaml | 49 +++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 52 deletions(-) delete mode 100644 .github/workflows/build-dev.yaml diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml deleted file mode 100644 index 62e0da98..00000000 --- a/.github/workflows/build-dev.yaml +++ /dev/null @@ -1,47 +0,0 @@ -name: "Build dockstatapi:nightly" - -on: - push: - branches: - - "dev" - -permissions: - packages: write - contents: read - -jobs: - build-dev: - runs-on: ubuntu-latest - steps: - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Github Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ github.token }} - - - name: Generate Docker tags - uses: docker/metadata-action@v5 - id: metadata - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,enable=true,priority=200,prefix=,suffix=,value=nightly - flavor: | - latest=false - - - name: Build and push - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64, - push: true - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 5d6e8d38..501ac9c3 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -5,7 +5,7 @@ on: [push] jobs: validation: runs-on: ubuntu-latest - name: Validation + name: "Validation" permissions: security-events: write packages: read @@ -42,7 +42,7 @@ jobs: CodeQL: needs: validation runs-on: ubuntu-latest - name: Analyze TypeScript + name: "Analyze TypeScript" permissions: security-events: write packages: read @@ -86,7 +86,7 @@ jobs: Anchore: needs: validation runs-on: ubuntu-latest - name: Anchore + name: "Anchore" permissions: security-events: write packages: read @@ -114,9 +114,9 @@ jobs: sarif_file: ./results.sarif test-building: - needs: validation + needs: [validation, Anchore, CodeQL] runs-on: ubuntu-latest - name: Test building + name: "Test building" permissions: security-events: write packages: read @@ -166,3 +166,42 @@ jobs: labels: ${{ steps.metadata.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + + build-dev: + name: "Dev-build" + runs-on: ubuntu-latest + if: github.ref_name == 'dev' + needs: [validation, test-building, Anchore, CodeQL] + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ github.token }} + + - name: Generate Docker tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,enable=true,priority=200,prefix=,suffix=,value=nightly + flavor: | + latest=false + + - name: Build and push + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64, + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From 3bf8da473822f54eacef67083052f54f589712dc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 31 Dec 2024 15:10:09 +0100 Subject: [PATCH 087/369] Fix: permissions --- .github/workflows/validation.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 501ac9c3..dd7357ca 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -169,6 +169,11 @@ jobs: build-dev: name: "Dev-build" + permissions: + security-events: read + packages: write + actions: read + contents: read runs-on: ubuntu-latest if: github.ref_name == 'dev' needs: [validation, test-building, Anchore, CodeQL] From 687e44aa0320d169d6a2a11280872228d58fbc55 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 31 Dec 2024 15:14:50 +0100 Subject: [PATCH 088/369] Feat: Update to ubuntu-24.04 in GHA --- .github/workflows/build-image.yaml | 2 +- .github/workflows/cloc.yaml | 2 +- .github/workflows/remove-stale.yaml | 2 +- .github/workflows/validation.yaml | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 9e382a29..720bed85 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -10,7 +10,7 @@ permissions: jobs: build-release: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml index 004f51b6..0f4245f9 100644 --- a/.github/workflows/cloc.yaml +++ b/.github/workflows/cloc.yaml @@ -10,7 +10,7 @@ on: jobs: cloc: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/remove-stale.yaml b/.github/workflows/remove-stale.yaml index 93d1acdc..47f9ae24 100644 --- a/.github/workflows/remove-stale.yaml +++ b/.github/workflows/remove-stale.yaml @@ -5,7 +5,7 @@ on: jobs: remove-stale: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/stale@v9 with: diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index dd7357ca..1abe640a 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -4,7 +4,7 @@ on: [push] jobs: validation: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: "Validation" permissions: security-events: write @@ -41,7 +41,7 @@ jobs: CodeQL: needs: validation - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: "Analyze TypeScript" permissions: security-events: write @@ -85,7 +85,7 @@ jobs: Anchore: needs: validation - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: "Anchore" permissions: security-events: write @@ -115,7 +115,7 @@ jobs: test-building: needs: [validation, Anchore, CodeQL] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: "Test building" permissions: security-events: write @@ -174,7 +174,7 @@ jobs: packages: write actions: read contents: read - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: github.ref_name == 'dev' needs: [validation, test-building, Anchore, CodeQL] steps: From dc2fed835e24c68d3fe72f70f3be306ace28a754 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 01:17:16 +0100 Subject: [PATCH 089/369] Feat: Custom User for Docker container --- Dockerfile | 25 ++++++++++++------------- Dockerfile-dev | 25 ++++++++++++------------- docker-compose.yaml | 5 ++++- src/misc/createEnvFile.sh | 0 src/misc/credits.sh | 0 src/misc/entrypoint.sh | 3 ++- src/misc/minifyDist.sh | 0 7 files changed, 30 insertions(+), 28 deletions(-) mode change 100644 => 100755 src/misc/createEnvFile.sh mode change 100644 => 100755 src/misc/credits.sh mode change 100644 => 100755 src/misc/minifyDist.sh diff --git a/Dockerfile b/Dockerfile index dc4f58cf..0e59a459 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,10 +14,7 @@ LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" WORKDIR /build ENV NODE_NO_WARNINGS=1 -RUN apk update && \ - apk upgrade && \ - apk add bash - +RUN apk add --update --no-cache bash COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ RUN npm install @@ -29,16 +26,13 @@ RUN npm run build:mini # Stage 2: main stage FROM alpine AS main -# Needed packages -RUN apk update && \ - apk upgrade && \ - apk add --update npm +RUN apk add --update npm WORKDIR /build RUN mkdir -p /build/src/data -COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ +COPY package*.json ./ RUN npm install --omit=dev COPY --from=builder /build/dist/* /build/src @@ -50,13 +44,18 @@ RUN node src/config/db.js # Stage 3: Production stage FROM alpine AS production -RUN apk add --update bash curl nodejs +WORKDIR /api + +RUN apk add --update --no-cache bash curl nodejs && \ + adduser -h /api -s /bin/bash -D dockstatapi dockstatapi && \ + chown -hR dockstatapi:dockstatapi /api + +USER dockstatapi + HEALTHCHECK --interval=5m --timeout=3s \ CMD curl -f http://localhost:9876/api/status || exit 1 -WORKDIR /api - -COPY --from=main /build /api +COPY --chown=dockstatapi:dockstatapi --from=main /build /api EXPOSE 9876 ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/Dockerfile-dev b/Dockerfile-dev index bd246884..ba9c01cb 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -14,10 +14,7 @@ LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" WORKDIR /build ENV NODE_NO_WARNINGS=1 -RUN apk update && \ - apk upgrade && \ - apk add bash - +RUN apk add --update --no-cache bash COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ RUN npm install @@ -29,16 +26,13 @@ RUN npm run build # Stage 2: main stage FROM alpine AS main -# Needed packages -RUN apk update && \ - apk upgrade && \ - apk add --update npm +RUN apk add --update npm WORKDIR /build RUN mkdir -p /build/src/data -COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ +COPY package*.json ./ RUN npm install --omit=dev COPY --from=builder /build/dist/* /build/src @@ -50,13 +44,18 @@ RUN node src/config/db.js # Stage 3: Production stage FROM alpine AS production -RUN apk add --update bash curl nodejs +WORKDIR /api + +RUN apk add --update --no-cache bash curl nodejs && \ + adduser -h /api -s /bin/bash -D dockstatapi dockstatapi && \ + chown -hR dockstatapi:dockstatapi /api + +USER dockstatapi + HEALTHCHECK --interval=5m --timeout=3s \ CMD curl -f http://localhost:9876/api/status || exit 1 -WORKDIR /api - -COPY --from=main /build /api +COPY --chown=dockstatapi:dockstatapi --from=main /build /api EXPOSE 9876 ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/docker-compose.yaml b/docker-compose.yaml index 06d1f459..4789b71c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -7,7 +7,7 @@ services: container_name: master environment: - NODE_ENV=development - - HA_MASTER=true + - HA_MASTER=false - HA_MASTER_IP=master:9876 - HA_NODE=slave:9876 - HA_UNSAFE=true @@ -21,6 +21,7 @@ services: depends_on: - slave - test-socket-proxy + slave: container_name: slave environment: @@ -30,6 +31,8 @@ services: ports: - 6789:9876 image: dockstatapi:local + depends_on: + - test-socket-proxy networks: - shared-network diff --git a/src/misc/createEnvFile.sh b/src/misc/createEnvFile.sh old mode 100644 new mode 100755 diff --git a/src/misc/credits.sh b/src/misc/credits.sh old mode 100644 new mode 100755 diff --git a/src/misc/entrypoint.sh b/src/misc/entrypoint.sh index 83eaf46d..60b8a0e4 100755 --- a/src/misc/entrypoint.sh +++ b/src/misc/entrypoint.sh @@ -1,3 +1,4 @@ +# entrypoint.sh: #!/bin/bash VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" @@ -24,6 +25,6 @@ DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simpl " -bash "./createEnvFile.sh" +bash ./createEnvFile.sh exec node src/server.js diff --git a/src/misc/minifyDist.sh b/src/misc/minifyDist.sh old mode 100644 new mode 100755 From 1f59fd4b5428f4151b87b93e7c484247ca178fd3 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 01:57:41 +0100 Subject: [PATCH 090/369] Fix: Lock acquisition needs exponential backoff to prevent thundering herd Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/controllers/highAvailability.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index 7bf7dc77..20e33de7 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -47,10 +47,25 @@ const configFiles: string[] = [ "./src/data/password.json", ]; +const MAX_RETRIES = 10; +const BASE_DELAY_MS = 100; + async function acquireLock(): Promise { + let retryCount = 0; + while (fs.existsSync(lockFilePath)) { - logger.warn("Lock file exists, waiting..."); - await sleep(100); + if (retryCount >= MAX_RETRIES) { + throw new Error("Failed to acquire lock: maximum retry attempts exceeded"); + } + + const backoffMs = BASE_DELAY_MS * Math.pow(2, retryCount); + // Add jitter to prevent thundering herd + const jitter = Math.random() * 0.3 * backoffMs; + const delayMs = backoffMs + jitter; + + logger.warn(`Lock file exists, waiting ${Math.round(delayMs)}ms before retry ${retryCount + 1}/${MAX_RETRIES}...`); + await sleep(delayMs); + retryCount++; } try { From a18b9d47158caa19617081c4ce69e4c4d49c758c Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 02:00:48 +0100 Subject: [PATCH 091/369] Feat: Daily rotating log files --- .github/workflows/validation.yaml | 4 +-- package-lock.json | 48 ++++++++++++++++++++++++++++++- package.json | 3 +- src/config/loggerConfig.ts | 9 +++++- 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 1abe640a..dfd9330a 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -63,7 +63,7 @@ jobs: - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: - languages: ${{ matrix.language }} + languages: javascript-typescript build-mode: ${{ matrix.build-mode }} queries: security-extended @@ -81,7 +81,7 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: - category: "/language:${{ matrix.language }}" + category: "/language:javascript-typescript" Anchore: needs: validation diff --git a/package-lock.json b/package-lock.json index ba55c01c..1310ab8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,8 @@ "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "winston": "^3.15.0" + "winston": "^3.15.0", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "@eslint/js": "^9.17.0", @@ -3031,6 +3032,15 @@ "node": ">=16.0.0" } }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -4512,6 +4522,15 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4869,6 +4888,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", @@ -6980,6 +7008,24 @@ "node": ">= 12.0.0" } }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, "node_modules/winston-transport": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", diff --git a/package.json b/package.json index 90422a28..6dd43aba 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "sqlite3": "^5.1.7", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "winston": "^3.15.0" + "winston": "^3.15.0", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/src/config/loggerConfig.ts b/src/config/loggerConfig.ts index 5d1a33e4..f2b30f43 100644 --- a/src/config/loggerConfig.ts +++ b/src/config/loggerConfig.ts @@ -1,4 +1,5 @@ import { createLogger, format, transports } from "winston"; +import DailyRotateFile from "winston-daily-rotate-file"; const gray = "\x1b[90m"; const reset = "\x1b[0m"; @@ -49,7 +50,13 @@ const logger = createLogger({ ), transports: [ new transports.Console(), - new transports.File({ filename: "logs/app.log" }), + new DailyRotateFile({ + filename: "logs/app-%DATE%.log", + datePattern: "YYYY-MM-DD", + maxSize: "20m", + maxFiles: "14d", + zippedArchive: true, + }), ], }); From a6838b08fe1442b624caa6466a9f00063ab32794 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 02:01:49 +0100 Subject: [PATCH 092/369] Feat: Better locking --- CREDITS.md | 73 +++++++++++++++-------------- src/controllers/highAvailability.ts | 8 +++- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index 050b430b..be34b479 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -4,48 +4,53 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) ### License: (MIT AND CC-BY-3.0) -| Name | Repository | Publisher | -| ----------------- | -------------------------------------------- | -------------------- | +| Name | Repository | Publisher | +|------|-------------|-----------| | spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | + ### License: Apache-2.0 -| Name | Repository | Publisher | -| ------------------------------------ | ------------------------------------------------------------- | --------------------- | -| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | -| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | -| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | -| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | -| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | -| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | -| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | -| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | -| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | +| Name | Repository | Publisher | +|------|-------------|-----------| +| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | +| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | +| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | +| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | +| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | +| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | +| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | +| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | +| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | +| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | + ### License: CC-BY-3.0 -| Name | Repository | Publisher | -| --------------------- | -------------------------------------------------- | -------------------- | +| Name | Repository | Publisher | +|------|-------------|-----------| | spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | + ### License: Python-2.0 -| Name | Repository | Publisher | -| -------------- | ---------------------------------- | --------- | -| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | +| Name | Repository | Publisher | +|------|-------------|-----------| +| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | + + diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index 20e33de7..c5e3325a 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -55,7 +55,9 @@ async function acquireLock(): Promise { while (fs.existsSync(lockFilePath)) { if (retryCount >= MAX_RETRIES) { - throw new Error("Failed to acquire lock: maximum retry attempts exceeded"); + throw new Error( + "Failed to acquire lock: maximum retry attempts exceeded", + ); } const backoffMs = BASE_DELAY_MS * Math.pow(2, retryCount); @@ -63,7 +65,9 @@ async function acquireLock(): Promise { const jitter = Math.random() * 0.3 * backoffMs; const delayMs = backoffMs + jitter; - logger.warn(`Lock file exists, waiting ${Math.round(delayMs)}ms before retry ${retryCount + 1}/${MAX_RETRIES}...`); + logger.warn( + `Lock file exists, waiting ${Math.round(delayMs)}ms before retry ${retryCount + 1}/${MAX_RETRIES}...`, + ); await sleep(delayMs); retryCount++; } From 3c348d34cdd5440a71c66b6f3dfce09e1ce1ad16 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 02:10:11 +0100 Subject: [PATCH 093/369] Fix: fixing the logger --- .gitignore | 1 + CREDITS.md | 73 +++++++++++++++++++++----------------------- src/utils/logger.ts | 74 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 98 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index 6c617861..84449de1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ docker .test* # Created by https://www.toptal.com/developers/gitignore/api/node ### Node ### +*-audit.json # Logs logs *.log diff --git a/CREDITS.md b/CREDITS.md index be34b479..050b430b 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -4,53 +4,48 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) ### License: (MIT AND CC-BY-3.0) -| Name | Repository | Publisher | -|------|-------------|-----------| +| Name | Repository | Publisher | +| ----------------- | -------------------------------------------- | -------------------- | | spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | - ### License: Apache-2.0 -| Name | Repository | Publisher | -|------|-------------|-----------| -| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | -| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | -| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | -| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | -| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | -| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | -| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | -| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | -| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | - +| Name | Repository | Publisher | +| ------------------------------------ | ------------------------------------------------------------- | --------------------- | +| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | +| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | +| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | +| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | +| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | +| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | +| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | +| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | +| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | +| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | +| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | ### License: CC-BY-3.0 -| Name | Repository | Publisher | -|------|-------------|-----------| +| Name | Repository | Publisher | +| --------------------- | -------------------------------------------------- | -------------------- | | spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | - ### License: Python-2.0 -| Name | Repository | Publisher | -|------|-------------|-----------| -| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | - - +| Name | Repository | Publisher | +| -------------- | ---------------------------------- | --------- | +| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | diff --git a/src/utils/logger.ts b/src/utils/logger.ts index e69955a9..8c1ea4a0 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,18 +1,70 @@ -import winston, { transport } from "winston"; +import { createLogger, format, transports } from "winston"; +import DailyRotateFile from "winston-daily-rotate-file"; import loggerConfig from "../config/loggerConfig"; -const transports: transport[] = [new winston.transports.Console()]; +// ANSI color codes for log level customization +const colors = { + gray: "\x1b[90m", + reset: "\x1b[0m", + white: "\x1b[97m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", +}; -transports.push( - new winston.transports.File({ - filename: "./logs/app.log", - }), -); +// Custom formatter to colorize log levels +function colorizeLogLevel(level: string, levelName: string) { + switch (level) { + case "info": + return `${colors.green}${levelName}${colors.reset}`; + case "debug": + return `${colors.blue}${levelName}${colors.reset}`; + case "error": + return `${colors.red}${levelName}${colors.reset}`; + case "warn": + return `${colors.yellow}${levelName}${colors.reset}`; + default: + return `${colors.gray}UNKNOWN${colors.reset}`; + } +} -const logger = winston.createLogger({ - level: loggerConfig.level, - format: loggerConfig.format, - transports, +// Filter out unwanted logs (example: Exit listeners logs) +const filterLogs = format((info) => { + if ( + typeof info.message === "string" && + info.message.includes("Exit listeners detected") + ) { + return false; + } + return info; +}); + +// Logger instance +const logger = createLogger({ + level: loggerConfig.level || "debug", + format: format.combine( + filterLogs(), + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + format.printf((info) => { + const level = info.level.toUpperCase().padEnd(5, " "); + const timestamp = `${colors.gray}${info.timestamp}${colors.reset}`; + const levelColorized = colorizeLogLevel(info.level.toLowerCase(), level); + const message = `${colors.white}${info.message}${colors.reset}`; + + return `${timestamp} ${levelColorized} : ${message}`; + }), + ), + transports: [ + new transports.Console(), + new DailyRotateFile({ + filename: "logs/app-%DATE%.log", + datePattern: "YYYY-MM-DD", + maxSize: "20m", + maxFiles: "14d", + zippedArchive: true, + }), + ], }); export default logger; From 59844643d243083ae10cdb7abcb8779a799ac400 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 18:29:56 +0100 Subject: [PATCH 094/369] Chore: Change routes to classes instead of huge functions --- TODO.md | 3 + src/controllers/auth.ts | 64 +++++++++++ src/controllers/containerController.ts | 32 +++--- src/handlers/api.ts | 138 ++++++++++++++++++++++++ src/handlers/auth.ts | 72 +++++++++++++ src/handlers/conf.ts | 97 +++++++++++++++++ src/handlers/data.ts | 93 ++++++++++++++++ src/handlers/frontend.ts | 138 ++++++++++++++++++++++++ src/handlers/ha.ts | 70 ++++++++++++ src/handlers/notification.ts | 73 +++++++++++++ src/handlers/response.ts | 41 +++++++ src/middleware/authMiddleware.ts | 13 ++- src/middleware/checkLock.ts | 10 +- src/routes/auth/routes.ts | 133 +++-------------------- src/routes/data/routes.ts | 89 ++------------- src/routes/frontendController/routes.ts | 115 ++++---------------- src/routes/getter/routes.ts | 125 +++------------------ src/routes/highavailability/routes.ts | 49 ++------- src/routes/notifications/routes.ts | 62 ++--------- src/routes/setter/routes.ts | 115 ++------------------ src/typings/dockerConfig.ts | 1 + src/typings/syncRequestBody.ts | 5 + src/typings/table.ts | 6 +- src/typings/template.ts | 5 + 24 files changed, 920 insertions(+), 629 deletions(-) create mode 100644 src/controllers/auth.ts create mode 100644 src/handlers/api.ts create mode 100644 src/handlers/auth.ts create mode 100644 src/handlers/conf.ts create mode 100644 src/handlers/data.ts create mode 100644 src/handlers/frontend.ts create mode 100644 src/handlers/ha.ts create mode 100644 src/handlers/notification.ts create mode 100644 src/handlers/response.ts create mode 100644 src/typings/syncRequestBody.ts create mode 100644 src/typings/template.ts diff --git a/TODO.md b/TODO.md index 36d32653..fc40ce6f 100644 --- a/TODO.md +++ b/TODO.md @@ -11,3 +11,6 @@ - [x] Better /api/status endpoint with connection status of each host - [x] Update notification service - [x] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) +- [ ] Better project structure +- [ ] Update logging => Better errors +- [ ] Update json responses and swagger diff --git a/src/controllers/auth.ts b/src/controllers/auth.ts new file mode 100644 index 00000000..905e39c9 --- /dev/null +++ b/src/controllers/auth.ts @@ -0,0 +1,64 @@ +import fs from "fs/promises"; +import logger from "../utils/logger"; +const passwordFile: string = "./src/data/password.json"; +const passwordBool: string = "./src/data/usePassword.txt"; + +async function authEnabled(): Promise { + let isAuthEnabled: boolean = false; + let data: string = ""; + try { + data = await fs.readFile(passwordBool, "utf8"); + isAuthEnabled = data.trim() === "true"; + return isAuthEnabled; + } catch (error: unknown) { + logger.error("Error reading file: ", error as Error); + return isAuthEnabled; + } +} + +async function readPasswordFile() { + let data: string = ""; + try { + data = await fs.readFile(passwordFile, "utf8"); + return data; + } catch (error: unknown) { + logger.error("Could not read saved password: ", error as Error); + return data; + } +} + +async function writePasswordFile(passwordData: string) { + try { + await fs.writeFile(passwordFile, passwordData); + setTrue(); + logger.debug("Authentication enabled"); + return "Authentication enabled"; + } catch (error: unknown) { + logger.error("Error writing password file:", error as Error); + return error; + } +} + +async function setTrue() { + try { + await fs.writeFile(passwordBool, "true", "utf8"); + logger.info(`Enabled authentication`); + return; + } catch (error: unknown) { + logger.error("Error writing to the file:", error as Error); + return; + } +} + +async function setFalse() { + try { + await fs.writeFile(passwordBool, "false", "utf8"); + logger.info(`Disabled authentication`); + return; + } catch (error: unknown) { + logger.error("Error writing to the file:", error as Error); + return; + } +} + +export { authEnabled, readPasswordFile, writePasswordFile, setFalse }; diff --git a/src/controllers/containerController.ts b/src/controllers/containerController.ts index ef1c8cef..61745e17 100644 --- a/src/controllers/containerController.ts +++ b/src/controllers/containerController.ts @@ -1,22 +1,25 @@ import getDockerClient from "../utils/dockerClient"; import logger from "../utils/logger"; import { Request, Response } from "express"; +import { createResponseHandler } from "../handlers/response"; const getContainers = async (req: Request, res: Response): Promise => { + const ResponseHandler = createResponseHandler(res); const host: string = (req.query.host as string) || "local"; + logger.info(`Fetching containers from host: ${host}`); + try { const docker = getDockerClient(host); const containers = await docker.listContainers(); - res.status(200).json(containers); - } catch (error: unknown) { - logger.error( - `Error fetching containers from host: ${host} - ${(error as Error).message || "Unknown error"} - Full error: ${JSON.stringify(error, null, 2)}`, + return ResponseHandler.rawData( + containers, + `Fetched containers from ${host}`, ); - res.status(500).json({ - error: `Error fetching containers: ${(error as Error).message || "Unknown error"}`, - }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); } }; @@ -28,21 +31,20 @@ const getContainerStats = async ( logger.info( `Fetching stats for container: ${containerID} from host: ${containerHost}`, ); + const ResponseHandler = createResponseHandler(res); + try { const docker = getDockerClient(containerHost); const container = docker.getContainer(containerID); const stats = await container.stats({ stream: false }); - logger.info( + + return ResponseHandler.rawData( + stats, `Successfully fetched stats for container: ${containerID} from host: ${containerHost}`, ); - res.status(200).json(stats); } catch (error: unknown) { - logger.error( - `Error fetching stats for container: ${containerID} from host: ${containerHost} - ${(error as Error).message}`, - ); - res.status(500).json({ - error: `Error fetching container stats: ${(error as Error).message}`, - }); + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); } }; diff --git a/src/handlers/api.ts b/src/handlers/api.ts new file mode 100644 index 00000000..37529688 --- /dev/null +++ b/src/handlers/api.ts @@ -0,0 +1,138 @@ +import extractRelevantData from "../utils/extractHostData"; +import { Request, Response } from "express"; +import getDockerClient from "../utils/dockerClient"; +import fetchAllContainers from "../utils/containerService"; +import { getCurrentSchedule } from "../controllers/scheduler"; +import fs from "fs"; +import checkReachability from "../utils/connectionChecker"; +const configPath = "./src/data/dockerConfig.json"; +const userConf = "./src/data/user.conf"; +import { dockerConfig } from "../typings/dockerConfig"; +import { createResponseHandler } from "./response"; + +class ApiHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + hosts() { + const ResponseHandler = createResponseHandler(this.res); + try { + const rawData = fs.readFileSync(configPath, "utf-8"); + const config: dockerConfig = JSON.parse(rawData); + + if (!config.hosts) { + return ResponseHandler.error("No hosts defined in configuration.", 400); + } + + const hosts = config.hosts.map((host) => host.name); + return ResponseHandler.rawData(hosts, "Fetched data for all hosts"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + system() { + const ResponseHandler = createResponseHandler(this.res); + try { + const rawData = fs.readFileSync(userConf, "utf8"); + const config = JSON.parse(rawData); + + if (!config) { + return ResponseHandler.error("Received empty configuration", 400); + } + + return ResponseHandler.rawData(config, "Fetched system configuration"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async hostStats(hostName: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + const docker = getDockerClient(hostName); + const info = await docker.info(); + const version = await docker.version(); + const relevantData = extractRelevantData({ hostName, info, version }); + + return ResponseHandler.rawData(relevantData, "Fetched Host stats"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async containers() { + const ResponseHandler = createResponseHandler(this.res); + try { + const allContainerData = await fetchAllContainers(); + return ResponseHandler.rawData( + allContainerData, + "Fetched all containers across all hosts", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async config() { + const ResponseHandler = createResponseHandler(this.res); + try { + const rawData = fs.readFileSync(configPath); + const data = JSON.parse(rawData.toString()); + return ResponseHandler.rawData(data, "Fetched config"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + currentSchedule() { + const ResponseHandler = createResponseHandler(this.res); + try { + const currentSchedule = getCurrentSchedule(); + return ResponseHandler.rawData( + currentSchedule, + "Fetched current schedule", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async status() { + const ResponseHandler = createResponseHandler(this.res); + try { + const data = await checkReachability(); + return ResponseHandler.rawData(data, "Fetched Status"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + frontendConfig() { + const configPath: string = "./src/data/frontendConfiguration.json"; + const ResponseHandler = createResponseHandler(this.res); + try { + const rawData = fs.readFileSync(configPath); + const data = JSON.parse(rawData.toString()); + ResponseHandler.rawData(data, "Fetched frontend configuration"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } +} + +export const createApiHandler = (req: Request, res: Response) => + new ApiHandler(req, res); diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts new file mode 100644 index 00000000..4dfbd3fb --- /dev/null +++ b/src/handlers/auth.ts @@ -0,0 +1,72 @@ +import { Request, Response } from "express"; +import { + authEnabled, + readPasswordFile, + writePasswordFile, + setFalse, +} from "../controllers/auth"; +import { createResponseHandler } from "./response"; +import bcrypt from "bcrypt"; + +const saltRounds: number = 10; + +class AuthenticationHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + async enable(password: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + if (await authEnabled()) { + return ResponseHandler.denied( + "Password Authentication is already enabled, please deactivate it first", + ); + } + + if (!password) { + return ResponseHandler.denied("Password is required"); + } + + const salt = await bcrypt.genSalt(saltRounds); + const hash = await bcrypt.hash(password, salt); + + const passwordData = { hash, salt }; + writePasswordFile(JSON.stringify(passwordData)); + + return ResponseHandler.ok("Authentication enabled successfully"); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async disable(password: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + if (!password) { + return ResponseHandler.denied("Password is required"); + } + + const storedData = JSON.parse(await readPasswordFile()); + const isPasswordValid = await bcrypt.compare(password, storedData.hash); + + if (!isPasswordValid) { + return ResponseHandler.error("Invalid password", 401); + } + + await setFalse(); + return ResponseHandler.ok("Authentication disabled successfully"); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } +} + +export const createAuthenticationHandler = (req: Request, res: Response) => + new AuthenticationHandler(req, res); diff --git a/src/handlers/conf.ts b/src/handlers/conf.ts new file mode 100644 index 00000000..e383c4d1 --- /dev/null +++ b/src/handlers/conf.ts @@ -0,0 +1,97 @@ +import { setFetchInterval, parseInterval } from "../controllers/scheduler"; +import { Request, Response } from "express"; +import fs from "fs"; +import { createResponseHandler } from "./response"; +import { target, dockerConfig } from "../typings/dockerConfig"; +const configPath: string = "./src/data/dockerConfig.json"; + +class ConfHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + addHost(req: Request) { + const ResponseHandler = createResponseHandler(this.res); + + try { + const { name, url, port } = req.query as unknown as target; + if (!name || !url || !port) { + return ResponseHandler.denied("Name, Port, and URL are required."); + } + + const config: dockerConfig = JSON.parse( + fs.readFileSync(configPath, "utf-8"), + ); + + if (config.hosts.some((host) => host.name === name)) { + return ResponseHandler.denied("Host already exists."); + } + + config.hosts.push({ name, url, port }); + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + + return ResponseHandler.ok("Host added successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + removeHost(req: Request) { + const ResponseHandler = createResponseHandler(this.res); + try { + const hostName = req.query.hostName as string; + + if (!hostName) { + return ResponseHandler.denied("Host name is required."); + } + + const currentState = fs.readFileSync(configPath, "utf-8"); + const config: dockerConfig = JSON.parse(currentState); + + const hostIndex = config.hosts.findIndex( + (host) => host.name === hostName, + ); + + if (hostIndex === -1) { + return ResponseHandler.error("Host not found.", 404); + } + + config.hosts.splice(hostIndex, 1); + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + + return ResponseHandler.ok("Host removed successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + scheduler(req: Request) { + const ResponseHandler = createResponseHandler(this.res); + try { + const interval = req.query.interval as string; + const newInterval = parseInterval(interval); + + if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { + return ResponseHandler.denied( + "Interval must be between 5 minutes and 6 hours.", + ); + } + + setFetchInterval(newInterval); + return ResponseHandler.ok("Updated interval"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } +} + +export const createConfHandler = (req: Request, res: Response) => + new ConfHandler(req, res); diff --git a/src/handlers/data.ts b/src/handlers/data.ts new file mode 100644 index 00000000..fd3515d6 --- /dev/null +++ b/src/handlers/data.ts @@ -0,0 +1,93 @@ +import { Response, Request } from "express"; +import db from "../config/db"; +import { Table, DataRow } from "../typings/table"; +import { createResponseHandler } from "./response"; + +function formatRows(rows: DataRow[]): Record { + return rows.reduce( + ( + acc: Record, + row, + index: number, + ): Record => { + acc[index] = JSON.parse(row.info); + return acc; + }, + {}, + ); +} + +class DatabaseHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + latest() { + const ResponseHandler = createResponseHandler(this.res); + db.get( + "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", + (error: unknown, row: Partial> | undefined) => { + if (error) { + return ResponseHandler.critical(error as string); + } + + if (!row || !row.info) { + return ResponseHandler.error( + "No data available for /data/latest", + 404, + ); + } + + try { + return ResponseHandler.rawData( + JSON.parse(row.info), + "Read latest data", + ); + } catch (error: unknown) { + const errorMsg = + error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + }, + ); + } + + all() { + const ResponseHandler = createResponseHandler(this.res); + const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + + db.all( + "SELECT info FROM data WHERE timestamp >= ?", + [oneDayAgo], + (error: unknown, rows: Pick[] | undefined) => { + if (error) { + return ResponseHandler.critical(error as string); + } + + if (!rows || rows.length === 0) { + return ResponseHandler.error("No data available", 404); + } + + return ResponseHandler.rawData(formatRows(rows), "Read database"); + }, + ); + } + + clear() { + const ResponseHandler = createResponseHandler(this.res); + db.run("DELETE FROM data", (error: unknown) => { + if (error) { + return ResponseHandler.critical(error as string); + } + + return ResponseHandler.ok("Database cleared successfully"); + }); + } +} + +export const createDatabaseHandler = (req: Request, res: Response) => + new DatabaseHandler(req, res); diff --git a/src/handlers/frontend.ts b/src/handlers/frontend.ts new file mode 100644 index 00000000..6b2edc55 --- /dev/null +++ b/src/handlers/frontend.ts @@ -0,0 +1,138 @@ +import { Request, Response } from "express"; +import { createResponseHandler } from "./response"; +import { + hideContainer, + unhideContainer, + addTagToContainer, + removeTagFromContainer, + pinContainer, + unpinContainer, + setLink, + removeLink, + setIcon, + removeIcon, +} from "../controllers/frontendConfiguration"; + +class FrontendHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + async show(containerName: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await unhideContainer(containerName); + return ResponseHandler.ok("Container unhidden successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async hide(containerName: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await hideContainer(containerName); + return ResponseHandler.ok("Hid container succesfully"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async addTag(containerName: string, tag: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await addTagToContainer(containerName, tag); + return ResponseHandler.ok("Tag added successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async removeTag(containerName: string, tag: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await removeTagFromContainer(containerName, tag); + ResponseHandler.ok("Tag removed successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async pin(containerName: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await pinContainer(containerName); + return ResponseHandler.ok("Container pinned successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async unPin(containerName: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await unpinContainer(containerName); + return ResponseHandler.ok("Container unpinned successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async addLink(containerName: string, link: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await setLink(containerName, link); + return ResponseHandler.ok("Link added successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async removeLink(containerName: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await removeLink(containerName); + return ResponseHandler.ok("Removed link succesfully"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async addIcon(containerName: string, icon: string, useCustomIcon: string) { + const ResponseHandler = createResponseHandler(this.res); + const iconBool = useCustomIcon === "true"; + try { + await setIcon(containerName, icon, iconBool); + return ResponseHandler.ok("Icon added successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async removeIcon(containerName: string) { + const ResponseHandler = createResponseHandler(this.res); + try { + await removeIcon(containerName); + return ResponseHandler.ok("Icon removed successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } +} + +export const createFrontendHandler = (req: Request, res: Response) => + new FrontendHandler(req, res); diff --git a/src/handlers/ha.ts b/src/handlers/ha.ts new file mode 100644 index 00000000..16c9ae19 --- /dev/null +++ b/src/handlers/ha.ts @@ -0,0 +1,70 @@ +import { Request, Response } from "express"; +import logger from "../utils/logger"; +import { + readConfig, + prepareFilesForSync, + ensureFileExists, +} from "../controllers/highAvailability"; +import { createResponseHandler } from "./response"; + +class HaHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + async config() { + const ResponseHandler = createResponseHandler(this.res); + try { + const data = await readConfig(); + return ResponseHandler.rawData(data, "Fetched HA-Config"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async sync(req: Request) { + const ResponseHandler = createResponseHandler(this.res); + try { + const { files } = req.body; + logger.info("Received synchronization request from master node."); + if (!files || typeof files !== "object") { + return ResponseHandler.error( + "Invalid request: 'files' object is missing or invalid.", + 400, + ); + } + + for (const [filePath, content] of Object.entries(files)) { + await ensureFileExists(filePath, content as string); + } + + return ResponseHandler.ok("Synchronization completed successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async prepare() { + const ResponseHandler = createResponseHandler(this.res); + try { + logger.info("Preparing files for synchronization."); + const fileData = await prepareFilesForSync(); + return ResponseHandler.rawData( + fileData, + "Done preparing files for synchronization", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } +} + +export const createHaHandler = (req: Request, res: Response) => + new HaHandler(req, res); diff --git a/src/handlers/notification.ts b/src/handlers/notification.ts new file mode 100644 index 00000000..ad5c2938 --- /dev/null +++ b/src/handlers/notification.ts @@ -0,0 +1,73 @@ +import { Request, Response } from "express"; +import fs from "fs"; +import notify from "../utils/notifications/_notify"; +const dataTemplate = "./src/data/template.json"; +import { TemplateData } from "../typings/template"; +import { createResponseHandler } from "./response"; + +function isTemplateData(data: TemplateData): data is TemplateData { + return ( + data !== null && typeof data === "object" && typeof data.text === "string" + ); +} + +class NotificationHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + getTemplate() { + const ResponseHandler = createResponseHandler(this.res); + try { + fs.readFile(dataTemplate, "utf-8", (error: unknown, data) => { + if (error) { + return ResponseHandler.error(error as string, 400); + } + return ResponseHandler.rawData(data, "Fetched notification template"); + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + setTemplate(req: Request) { + const ResponseHandler = createResponseHandler(this.res); + const newTemplate: TemplateData = req.body; + + try { + if (!isTemplateData(newTemplate)) { + return ResponseHandler.error( + "Invalid input format. Expected JSON with a 'text' field.", + 400, + ); + } + + fs.writeFileSync(dataTemplate, JSON.stringify(newTemplate, null, 2)); + return ResponseHandler.ok("Template updated successfully."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async test(req: Request) { + const { type, containerId } = req.params; + const ResponseHandler = createResponseHandler(this.res); + + try { + await notify(type, containerId); + return ResponseHandler.ok("Sent test notification"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } +} + +export const createNotificationHandler = (req: Request, res: Response) => + new NotificationHandler(req, res); diff --git a/src/handlers/response.ts b/src/handlers/response.ts new file mode 100644 index 00000000..8c6e95b8 --- /dev/null +++ b/src/handlers/response.ts @@ -0,0 +1,41 @@ +import { Response } from "express"; +import logger from "../utils/logger"; + +class ResponseHandler { + private res: Response; + + constructor(res: Response) { + this.res = res; + } + + rawData(data: unknown, message: string) { + logger.info(message); + this.res.status(200).json(data); + } + + ok(message: string) { + logger.info(message); + this.res.status(200).json({ status: "success", message }); + } + + denied(message: string) { + logger.warn(message); + this.res.status(403).json({ status: "denied", message }); + } + + error(message: string, code: number) { + logger.error(`Code: ${code} - ${message}`); + this.res.status(code).json({ status: "error", message }); + } + + critical(log: string) { + logger.error(log); + this.res.status(500).json({ + status: "critical", + message: "Please see the server logs for more info", + }); + } +} + +export const createResponseHandler = (res: Response) => + new ResponseHandler(res); diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts index 500a7fa8..4afb3939 100644 --- a/src/middleware/authMiddleware.ts +++ b/src/middleware/authMiddleware.ts @@ -2,7 +2,7 @@ import bcrypt from "bcrypt"; import { Request, Response, NextFunction } from "express"; import logger from "../utils/logger"; import { rateLimitedReadFile } from "../utils/rateLimitFS"; - +import { createResponseHandler } from "../handlers/response"; const passwordFile = "./src/data/password.json"; const passwordBool = "./src/data/usePassword.txt"; @@ -11,6 +11,7 @@ async function authMiddleware( res: Response, next: NextFunction, ): Promise { + const ResponseHandler = createResponseHandler(res); try { const authStatusData = await rateLimitedReadFile(passwordBool); const isAuthEnabled = authStatusData.trim() === "true"; @@ -23,8 +24,7 @@ async function authMiddleware( const providedPassword = req.headers["x-password"]; if (!providedPassword) { - logger.error("Password required - Denied"); - res.status(401).json({ message: "Password required" }); + ResponseHandler.denied("Password required"); return; } @@ -36,16 +36,15 @@ async function authMiddleware( storedData.hash, ); if (!passwordMatch) { - logger.error("Invalid Password - Denied access"); - res.status(401).json({ message: "Invalid password" }); + ResponseHandler.denied("Invalid Password"); return; } logger.debug("Authentication succesfull"); next(); } catch (error: unknown) { - logger.error("Error in authMiddleware:", error); - res.status(500).json({ message: "Internal server error" }); + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); } } diff --git a/src/middleware/checkLock.ts b/src/middleware/checkLock.ts index 73740a07..c01540fe 100644 --- a/src/middleware/checkLock.ts +++ b/src/middleware/checkLock.ts @@ -1,5 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { rateLimitedExistsSync } from "../utils/rateLimitFS"; +import { createResponseHandler } from "../handlers/response"; const lockFilePath = "./src/data/ha.lock"; @@ -8,11 +9,12 @@ export async function blockWhileLocked( res: Response, next: NextFunction, ): Promise { + const ResponseHandler = createResponseHandler(res); if (await rateLimitedExistsSync(lockFilePath)) { - res.status(503).json({ - error: - "Service unavailable. The high-availability lock is currently active. Please try again later.", - }); + ResponseHandler.error( + "Service unavailable. The high-availability lock is currently active. Please try again later.", + 503, + ); } else { next(); } diff --git a/src/routes/auth/routes.ts b/src/routes/auth/routes.ts index f7e0b18e..47ff6f25 100644 --- a/src/routes/auth/routes.ts +++ b/src/routes/auth/routes.ts @@ -1,69 +1,7 @@ import { Router, Request, Response } from "express"; -import bcrypt from "bcrypt"; -import fs from "fs/promises"; -import logger from "../../utils/logger"; -const passwordFile: string = "./src/data/password.json"; -const passwordBool: string = "./src/data/usePassword.txt"; -const saltRounds: number = 10; -const router: Router = Router(); +import { createAuthenticationHandler } from "../../handlers/auth"; -async function authEnabled(): Promise { - let isAuthEnabled: boolean = false; - let data: string = ""; - try { - data = await fs.readFile(passwordBool, "utf8"); - isAuthEnabled = data.trim() === "true"; - return isAuthEnabled; - } catch (error: unknown) { - logger.error("Error reading file: ", error as Error); - return isAuthEnabled; - } -} - -async function readPasswordFile() { - let data: string = ""; - try { - data = await fs.readFile(passwordFile, "utf8"); - return data; - } catch (error: unknown) { - logger.error("Could not read saved password: ", error as Error); - return data; - } -} - -async function writePasswordFile(passwordData: string) { - try { - await fs.writeFile(passwordFile, passwordData); - setTrue(); - logger.debug("Authentication enabled"); - return "Authentication enabled"; - } catch (error: unknown) { - logger.error("Error writing password file:", error as Error); - return error; - } -} - -async function setTrue() { - try { - await fs.writeFile(passwordBool, "true", "utf8"); - logger.info(`Enabled authentication`); - return; - } catch (error: unknown) { - logger.error("Error writing to the file:", error as Error); - return; - } -} - -async function setFalse() { - try { - await fs.writeFile(passwordBool, "false", "utf8"); - logger.info(`Disabled authentication`); - return; - } catch (error: unknown) { - logger.error("Error writing to the file:", error as Error); - return; - } -} +const router = Router(); /** * @swagger @@ -75,6 +13,8 @@ async function setFalse() { * - name: password * in: query * required: true + * schema: + * type: string * responses: * 200: * description: Authentication enabled. @@ -84,39 +24,9 @@ async function setFalse() { * description: Error saving password. */ router.post("/enable", async (req: Request, res: Response): Promise => { - try { - const password = req.query.password as string; - - if (await authEnabled()) { - logger.error( - "Password Authentication is already enabled, please deactivate it first", - ); - res.status(401).json({ - message: - "Password Authentication is already enabled, please deactivate it first", - }); - return; - } - - if (!password) { - logger.error("Password is required"); - res.status(400).json({ message: "Password is required" }); - return; - } - - const salt = await bcrypt.genSalt(saltRounds); - const hash = await bcrypt.hash(password, salt); - - const passwordData = { hash, salt }; - writePasswordFile(JSON.stringify(passwordData)); - - res - .status(200) - .json({ message: "Password Authentication enabled successfully" }); - } catch (error: unknown) { - logger.error(`Error enabling password authentication: ${error as Error}`); - res.status(500).json({ message: "An error occurred" }); - } + const password = req.query.password as string; + const handler = createAuthenticationHandler(req, res); + await handler.enable(password); }); /** @@ -129,6 +39,8 @@ router.post("/enable", async (req: Request, res: Response): Promise => { * - name: password * in: query * required: true + * schema: + * type: string * responses: * 200: * description: Authentication disabled. @@ -140,30 +52,9 @@ router.post("/enable", async (req: Request, res: Response): Promise => { * description: Error disabling authentication. */ router.post("/disable", async (req: Request, res: Response): Promise => { - try { - const password = req.query.password as string; - - if (!password) { - logger.error("Password is required!"); - res.status(400).json({ message: "Password is required" }); - return; - } - - const storedData = JSON.parse(await readPasswordFile()); - - const isPasswordValid = await bcrypt.compare(password, storedData.hash); - if (!isPasswordValid) { - logger.error("Invalid password"); - res.status(401).json({ message: "Invalid password" }); - return; - } - - await setFalse(); // Assuming this is an async function - res.status(200).json({ message: "Authentication disabled" }); - } catch (error: unknown) { - logger.error(`Error disabling authentication: ${error as Error}`); - res.status(500).json({ message: "An error occurred" }); - } + const password = req.query.password as string; + const handler = createAuthenticationHandler(req, res); + await handler.disable(password); }); export default router; diff --git a/src/routes/data/routes.ts b/src/routes/data/routes.ts index 108fafe4..92a7f976 100644 --- a/src/routes/data/routes.ts +++ b/src/routes/data/routes.ts @@ -1,26 +1,6 @@ -import express from "express"; +import express, { Request, Response } from "express"; const router = express.Router(); -import db from "../../config/db"; -import logger from "../../utils/logger"; -import Table from "../../typings/table"; - -interface DataRow { - info: string; -} - -function formatRows(rows: DataRow[]): Record { - return rows.reduce( - ( - acc: Record, - row, - index: number, - ): Record => { - acc[index] = JSON.parse(row.info); - return acc; - }, - {}, - ); -} +import { createDatabaseHandler } from "../../handlers/data"; /** * @swagger @@ -90,29 +70,9 @@ function formatRows(rows: DataRow[]): Record { * description: Networking mode for the container * example: "bridge" */ -router.get("/latest", (req, res) => { - db.get( - "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", - (error: unknown, row: Partial> | undefined) => { - if (error) { - logger.error("Error fetching latest data:", (error as Error).message); - return res.status(500).json({ error: "Internal server error" }); - } - - if (!row || !row.info) { - logger.warn("No data available for /data/latest"); - return res.status(404).json({ error: "No data available" }); - } - - logger.debug("Fetching /data/latest"); - try { - res.json(JSON.parse(row.info)); - } catch (error: unknown) { - logger.error("Error parsing data:", (error as Error).message); - res.status(500).json({ error: "Data format error" }); - } - }, - ); +router.get("/latest", (req: Request, res: Response) => { + const DatabaseHandler = createDatabaseHandler(req, res); + return DatabaseHandler.latest(); }); /** @@ -162,30 +122,9 @@ router.get("/latest", (req, res) => { * type: number * example: 3072 */ -router.get("/all", (req, res) => { - const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); - - db.all( - "SELECT info FROM data WHERE timestamp >= ?", - [oneDayAgo], - (error: unknown, rows: Pick[] | undefined) => { - if (error) { - logger.error( - "Error fetching data from last 24 hours:", - (error as Error).message, - ); - return res.status(500).json({ error: "Internal server error" }); - } - - logger.debug("Fetching /data/time/24h"); - if (!rows || rows.length === 0) { - logger.warn("No data available for /data/time/24h"); - return res.status(404).json({ error: "No data available" }); - } - - res.json(formatRows(rows)); - }, - ); +router.get("/all", (req: Request, res: Response) => { + const DatabaseHandler = createDatabaseHandler(req, res); + return DatabaseHandler.all(); }); /** @@ -207,15 +146,9 @@ router.get("/all", (req, res) => { * description: Success message upon database clearance * example: "Database cleared successfully." */ -router.delete("/clear", (req, res) => { - db.run("DELETE FROM data", (error: unknown) => { - if (error) { - logger.error("Error clearing the database:", (error as Error).message); - return res.status(500).json({ error: "Internal server error" }); - } - logger.debug("Database cleared successfully"); - res.json({ message: "Database cleared successfully" }); - }); +router.delete("/clear", (req: Request, res: Response) => { + const DatabaseHandler = createDatabaseHandler(req, res); + return DatabaseHandler.clear(); }); export default router; diff --git a/src/routes/frontendController/routes.ts b/src/routes/frontendController/routes.ts index 0de95fe9..39500c51 100644 --- a/src/routes/frontendController/routes.ts +++ b/src/routes/frontendController/routes.ts @@ -1,25 +1,6 @@ import express from "express"; const router = express.Router(); -import { - hideContainer, - unhideContainer, - addTagToContainer, - removeTagFromContainer, - pinContainer, - unpinContainer, - setLink, - removeLink, - setIcon, - removeIcon, -} from "../../controllers/frontendConfiguration"; - -/* -____ ___ ____ _____ -| _ \ / _ \/ ___|_ _| -| |_) | | | \___ \ | | -| __/| |_| |___) || | -|_| \___/|____/ |_| -*/ +import { createFrontendHandler } from "../../handlers/frontend"; /** * @swagger @@ -62,15 +43,10 @@ ____ ___ ____ _____ * type: string * description: Error message */ -// Unhide a container router.post("/show/:containerName", async (req, res) => { - const { containerName } = req.params; - try { - await unhideContainer(containerName); - res.status(200).json({ message: "Container unhidden successfully." }); - } catch (error: unknown) { - res.status(500).json({ error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + const containerName = req.params.containerName; + return FrontendHandler.show(containerName); }); /** @@ -120,15 +96,10 @@ router.post("/show/:containerName", async (req, res) => { * type: string * description: Error message */ -// Add a tag to a container router.post("/tag/:containerName/:tag", async (req, res) => { const { containerName, tag } = req.params; - try { - await addTagToContainer(containerName, tag); - res.json({ success: true, message: "Tag added successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.addTag(containerName, tag); }); /** @@ -172,15 +143,10 @@ router.post("/tag/:containerName/:tag", async (req, res) => { * type: string * description: Error message */ -// Pin a container router.post("/pin/:containerName", async (req, res) => { const { containerName } = req.params; - try { - await pinContainer(containerName); - res.json({ success: true, message: "Container pinned successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.pin(containerName); }); /** @@ -230,15 +196,10 @@ router.post("/pin/:containerName", async (req, res) => { * type: string * description: Error message */ -// Add link to container router.post("/add-link/:containerName/:link", async (req, res) => { const { containerName, link } = req.params; - try { - await setLink(containerName, link); - res.json({ success: true, message: "Link added successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.addLink(containerName, link); }); /** @@ -294,19 +255,12 @@ router.post("/add-link/:containerName/:link", async (req, res) => { * type: string * description: Error message */ -// Add Icon to container router.post( "/add-icon/:containerName/:icon/:useCustomIcon", async (req, res) => { const { containerName, icon, useCustomIcon } = req.params; - try { - const custom = useCustomIcon === "true"; - - await setIcon(containerName, icon, custom); - res.json({ success: true, message: "Icon added successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.addIcon(containerName, icon, useCustomIcon); }, ); @@ -362,13 +316,8 @@ router.post( // Hide a container router.delete("/hide/:containerName", async (req, res) => { const { containerName } = req.params; - const target = containerName; - try { - await hideContainer(target); - res.json({ success: true, message: `Container, ${target}, hidden.` }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.hide(containerName); }); /** @@ -418,15 +367,10 @@ router.delete("/hide/:containerName", async (req, res) => { * type: string * description: Error message */ -// Remove a tag from a container router.delete("/remove-tag/:containerName/:tag", async (req, res) => { const { containerName, tag } = req.params; - try { - await removeTagFromContainer(containerName, tag); - res.json({ success: true, message: "Tag removed successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.removeTag(containerName, tag); }); /** @@ -470,15 +414,10 @@ router.delete("/remove-tag/:containerName/:tag", async (req, res) => { * type: string * description: Error message */ -// Unpin a container router.delete("/unpin/:containerName", async (req, res) => { const { containerName } = req.params; - try { - await unpinContainer(containerName); - res.json({ success: true, message: "Container unpinned successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.unPin(containerName); }); /** @@ -522,15 +461,10 @@ router.delete("/unpin/:containerName", async (req, res) => { * type: string * description: Error message */ -// Remove link from container router.delete("/remove-link/:containerName", async (req, res) => { const { containerName } = req.params; - try { - await removeLink(containerName); - res.json({ success: true, message: "Link removed successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.removeLink(containerName); }); /** @@ -574,15 +508,10 @@ router.delete("/remove-link/:containerName", async (req, res) => { * type: string * description: Error message */ -// Remove icon from container router.delete("/remove-icon/:containerName", async (req, res) => { const { containerName } = req.params; - try { - await removeIcon(containerName); - res.json({ success: true, message: "Icon removed successfully." }); - } catch (error: unknown) { - res.status(500).json({ success: false, error: (error as Error).message }); - } + const FrontendHandler = createFrontendHandler(req, res); + return FrontendHandler.removeIcon(containerName); }); export default router; diff --git a/src/routes/getter/routes.ts b/src/routes/getter/routes.ts index d278075c..0912d48b 100644 --- a/src/routes/getter/routes.ts +++ b/src/routes/getter/routes.ts @@ -1,15 +1,6 @@ -import extractRelevantData from "../../utils/extractHostData"; import { Router, Request, Response } from "express"; -import getDockerClient from "../../utils/dockerClient"; -import fetchAllContainers from "../../utils/containerService"; -import { getCurrentSchedule } from "../../controllers/scheduler"; -import logger from "../../utils/logger"; -import fs from "fs"; -import checkReachability from "../../utils/connectionChecker"; -const configPath = "./src/data/dockerConfig.json"; +import { createApiHandler } from "../../handlers/api"; const router = Router(); -const userConf = "./src/data/user.conf"; -import { dockerConfig } from "../../typings/dockerConfig"; /** * @swagger @@ -32,22 +23,8 @@ import { dockerConfig } from "../../typings/dockerConfig"; * example: ["local", "remote1"] */ router.get("/hosts", (req: Request, res: Response) => { - logger.info(`Fetching config: ${configPath}`); - try { - const rawData = fs.readFileSync(configPath, "utf-8"); - const config: dockerConfig = JSON.parse(rawData); - - if (!config.hosts) { - throw new Error("No hosts defined in configuration."); - } - - const hosts = config.hosts.map((host) => host.name); - logger.debug("Fetching all available Docker hosts"); - res.status(200).json({ hosts }); - } catch (error: unknown) { - logger.error("Error fetching hosts: " + (error as Error).message); - res.status(500).json({ error: "Failed to fetch Docker hosts" }); - } + const ApiHandler = createApiHandler(req, res); + return ApiHandler.hosts(); }); /** @@ -76,20 +53,8 @@ router.get("/hosts", (req: Request, res: Response) => { * description: Error message detailing the issue encountered. */ router.get("/system", (req: Request, res: Response) => { - logger.info(`Fetching ${userConf}`); - - try { - const rawData = fs.readFileSync(userConf, "utf8"); - const config = JSON.parse(rawData); - - if (!config) { - res.status(500).json({ error: `Error received empty ${userConf}` }); - } - res.status(200).json(config); - } catch (error: unknown) { - logger.error(`Could not fetch ${userConf}: ${error as Error}`); - res.status(500).json({ error: `Failed to fetch ${userConf}` }); - } + const ApiHandler = createApiHandler(req, res); + return ApiHandler.system(); }); /** @@ -135,22 +100,8 @@ router.get("/system", (req: Request, res: Response) => { */ router.get("/host/:hostName/stats", async (req: Request, res: Response) => { const { hostName } = req.params; - logger.info(`Fetching stats for host: ${hostName}`); - try { - const docker = getDockerClient(hostName); - const info = await docker.info(); - const version = await docker.version(); - const relevantData = extractRelevantData({ hostName, info, version }); - - res.status(200).json(relevantData); - } catch (error: unknown) { - logger.error( - `Error fetching stats for host: ${hostName} - ${(error as Error).message || "Unknown error"}`, - ); - res.status(500).json({ - error: `Error fetching host stats: ${(error as Error).message || "Unknown error"}`, - }); - } + const ApiHandler = createApiHandler(req, res); + return ApiHandler.hostStats(hostName); }); /** @@ -227,15 +178,8 @@ router.get("/host/:hostName/stats", async (req: Request, res: Response) => { * description: Error message detailing the issue encountered. */ router.get("/containers", async (req: Request, res: Response) => { - logger.info("Fetching all containers across all hosts"); - try { - const allContainerData = await fetchAllContainers(); - logger.debug("Fetched /api/containers"); - res.status(200).json(allContainerData); - } catch (error: unknown) { - logger.error(`Error fetching containers: ${(error as Error).message}`); - res.status(500).json({ error: "Failed to fetch containers" }); - } + const ApiHandler = createApiHandler(req, res); + return ApiHandler.containers(); }); /** @@ -264,17 +208,8 @@ router.get("/containers", async (req: Request, res: Response) => { * description: Error message detailing the issue encountered. */ router.get("/config", async (req: Request, res: Response) => { - try { - const rawData = fs.readFileSync(configPath); - const jsonData = JSON.parse(rawData.toString()); - logger.debug("Fetching /api/config"); - res.status(200).json(jsonData); - } catch (error: unknown) { - logger.error( - "Error loading dockerConfig.json: " + (error as Error).message, - ); - res.status(500).json({ error: "Failed to load Docker configuration" }); - } + const ApiHandler = createApiHandler(req, res); + return ApiHandler.config(); }); /** @@ -296,9 +231,8 @@ router.get("/config", async (req: Request, res: Response) => { * description: Current fetch interval in seconds. */ router.get("/current-schedule", (req: Request, res: Response) => { - const currentSchedule = getCurrentSchedule(); - logger.debug("Fetching current shedule"); - res.json(currentSchedule); + const ApiHandler = createApiHandler(req, res); + return ApiHandler.currentSchedule(); }); /** @@ -331,13 +265,8 @@ router.get("/current-schedule", (req: Request, res: Response) => { */ router.get("/status", async (req: Request, res: Response) => { - logger.debug("Fetching /api/status"); - try { - const jsonData = await checkReachability(); - res.status(200).json(jsonData); - } catch (error: unknown) { - logger.error(`Error while fetching data: ${error as Error}`); - } + const ApiHandler = createApiHandler(req, res); + return ApiHandler.status(); }); /** @@ -383,28 +312,8 @@ router.get("/status", async (req: Request, res: Response) => { * description: Error message */ router.get("/frontend-config", (req: Request, res: Response) => { - const configPath: string = "./src/data/frontendConfiguration.json"; - - fs.stat(configPath, (exists) => { - if (exists == null) { - logger.debug(`${configPath} exists, trying to read it`); - } else if (exists.code === "ENOENT") { - logger.warn(`${configPath} doesn't exist, trying to create it`); - fs.promises.writeFile(configPath, JSON.stringify([], null, 2), "utf-8"); - } - }); - - try { - const rawData = fs.readFileSync(configPath); - const jsonData = JSON.parse(rawData.toString()); - - res.status(200).json(jsonData); - } catch (error: unknown) { - logger.error( - "Error loading frontendConfiguration.json: " + (error as Error).message, - ); - res.status(500).json({ error: "Failed to load Frontend configuration" }); - } + const ApiHandler = createApiHandler(req, res); + return ApiHandler.frontendConfig(); }); export default router; diff --git a/src/routes/highavailability/routes.ts b/src/routes/highavailability/routes.ts index 3fadb02e..86057bcd 100644 --- a/src/routes/highavailability/routes.ts +++ b/src/routes/highavailability/routes.ts @@ -1,16 +1,6 @@ -// File: /src/routes/ha/routes.ts import { Router, Request, Response } from "express"; -import logger from "../../utils/logger"; -import { - readConfig, - prepareFilesForSync, - ensureFileExists, -} from "../../controllers/highAvailability"; - -interface SyncRequestBody { - files: Record; -} - +import { SyncRequestBody } from "../../typings/syncRequestBody"; +import { createHaHandler } from "../../handlers/ha"; const router = Router(); /** @@ -24,9 +14,8 @@ const router = Router(); * description: A JSON object containing the config. */ router.get("/config", async (req: Request, res: Response) => { - logger.info("Getting the HA-Config"); - const data = await readConfig(); - res.status(200).json(data); + const HaHandler = createHaHandler(req, res); + return HaHandler.config(); }); /** @@ -45,29 +34,8 @@ router.post( req: Request<{}, {}, SyncRequestBody>, // eslint-disable-line res: Response, ): Promise => { - try { - const { files } = req.body; - - if (!files || typeof files !== "object") { - const errorMsg = - "Invalid request: 'files' object is missing or invalid."; - logger.error(errorMsg); - res.status(400).json({ message: errorMsg }); - return; - } - - logger.info("Received synchronization request from master node."); - - for (const [filePath, content] of Object.entries(files)) { - await ensureFileExists(filePath, content); - } - - logger.info("Synchronization completed successfully."); - res.status(200).json({ message: "Synchronization completed." }); - } catch (error) { - logger.error(`Error during synchronization: ${(error as Error).message}`); - res.status(500).json({ message: "Synchronization failed." }); - } + const HaHandler = createHaHandler(req, res); + return HaHandler.sync(req); }, ); @@ -82,9 +50,8 @@ router.post( * description: A JSON object containing files to sync. */ router.get("/prepare-sync", async (req: Request, res: Response) => { - logger.info("Preparing files for synchronization."); - const fileData = await prepareFilesForSync(); - res.status(200).json(fileData); + const HaHandler = createHaHandler(req, res); + return HaHandler.prepare(); }); export default router; diff --git a/src/routes/notifications/routes.ts b/src/routes/notifications/routes.ts index 17cf6986..4544b8ce 100644 --- a/src/routes/notifications/routes.ts +++ b/src/routes/notifications/routes.ts @@ -1,26 +1,7 @@ import { Request, Response, Router } from "express"; -import logger from "../../utils/logger"; -import fs from "fs"; -import notify from "../../utils/notifications/_notify"; -const dataTemplate = "./src/data/template.json"; +import { createNotificationHandler } from "../../handlers/notification"; const router = Router(); -/////////// -// Will be moved! - -interface TemplateData { - text: string; -} - -function isTemplateData(data: TemplateData): data is TemplateData { - return ( - data !== null && typeof data === "object" && typeof data.text === "string" - ); -} - -// Will be moved -/////////// - /** * @swagger * /notification-service/get-template: @@ -53,13 +34,8 @@ function isTemplateData(data: TemplateData): data is TemplateData { * description: Error message */ router.get("/get-template", (req: Request, res: Response) => { - fs.readFile(dataTemplate, "utf-8", (error, data) => { - if (error) { - logger.error("Errored opening:", error); - return res.status(500).json({ message: `Error opening: ${error}` }); - } - res.json(JSON.parse(data)); - }); + const NotificationHandler = createNotificationHandler(req, res); + return NotificationHandler.getTemplate(); }); /** @@ -98,27 +74,8 @@ router.get("/get-template", (req: Request, res: Response) => { * description: Error message */ router.post("/set-template", (req: Request, res: Response): void => { - const newData: TemplateData = req.body; - - if (!isTemplateData(newData)) { - res.status(400).json({ - message: "Invalid input format. Expected JSON with a 'text' field.", - }); - return; - } - - fs.promises - .writeFile(dataTemplate, JSON.stringify(newData, null, 2), "utf-8") - .then(() => { - logger.info("Template updated successfully."); - res.json({ message: "Template updated successfully." }); - }) - .catch((error) => { - logger.error("Error writing to file: " + error.message); - res - .status(500) - .json({ message: `Error writing to file: ${error.message}` }); - }); + const NotificationHandler = createNotificationHandler(req, res); + return NotificationHandler.setTemplate(req); }); /** @@ -165,13 +122,8 @@ router.post("/set-template", (req: Request, res: Response): void => { * type: string */ router.post("/test/:type/:containerId", async (req: Request, res: Response) => { - const { type, containerId } = req.params; - try { - await notify(type, containerId); - res.json({ success: true, message: `Sent test notification to ${type}` }); - } catch (error: unknown) { - res.json({ success: false, message: `Errored: ${error as Error}` }); - } + const NotificationHandler = createNotificationHandler(req, res); + NotificationHandler.test(req); }); export default router; diff --git a/src/routes/setter/routes.ts b/src/routes/setter/routes.ts index 96915a92..75ef747d 100644 --- a/src/routes/setter/routes.ts +++ b/src/routes/setter/routes.ts @@ -1,20 +1,6 @@ -import { setFetchInterval, parseInterval } from "../../controllers/scheduler"; -import logger from "../../utils/logger"; import express, { Router, Request, Response } from "express"; -import fs from "fs"; - +import { createConfHandler } from "../../handlers/conf"; const router: Router = express.Router(); -const configPath: string = "./src/data/dockerConfig.json"; - -interface Host { - name: string; - url: string; - port: string; -} - -interface DockerConfig { - hosts: Host[]; -} /** * @swagger @@ -43,46 +29,10 @@ interface DockerConfig { * 500: * description: An error occurred while adding the host. */ - -router.put( - "/addHost", - async ( - req: Request< - unknown, - unknown, - unknown, - { name: string; url: string; port: string } - >, - res: Response, - ): Promise => { - const { name, url, port } = req.query; - - if (!name || !url || !port) { - res.status(400).json({ error: "Name, Port, and URL are required." }); - return; - } - - try { - const config: DockerConfig = JSON.parse( - fs.readFileSync(configPath, "utf-8"), - ); - - if (config.hosts.some((host) => host.name === name)) { - res.status(400).json({ error: "Host already exists." }); - return; - } - - config.hosts.push({ name, url, port }); - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); - logger.info(`Added new host: ${name}`); - res.status(200).json({ message: "Host added successfully." }); - } catch (error: unknown) { - const err = error as Error; - logger.error("Error adding host: " + err.message); - res.status(500).json({ error: "Failed to add host." }); - } - }, -); +router.put("/addHost", async (req: Request, res: Response): Promise => { + const ConfHandler = createConfHandler(req, res); + return ConfHandler.addHost(req); +}); /** * @swagger @@ -102,24 +52,8 @@ router.put( * description: Invalid interval format or out of range. */ router.put("/scheduler", (req: Request, res: Response) => { - const interval = req.query.interval as string; - - try { - const newInterval = parseInterval(interval); - - if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { - res - .status(400) - .json({ error: "Interval must be between 5 minutes and 6 hours." }); - } - - setFetchInterval(newInterval); - res.status(200).json({ message: `Fetch interval set to ${interval}.` }); - } catch (error: unknown) { - const err = error as Error; - logger.error("Error setting fetch interval: " + err.message); - res.status(400).json({ error: "Invalid interval format." }); - } + const ConfHandler = createConfHandler(req, res); + return ConfHandler.scheduler(req); }); /** @@ -142,39 +76,8 @@ router.put("/scheduler", (req: Request, res: Response) => { * description: An error occurred while removing the host. */ router.delete("/removeHost", (req: Request, res: Response): void => { - const hostName = req.query.hostName as string; - - if (!hostName) { - res.status(400).json({ error: "Host name is required." }); - return; - } - - fs.promises - .readFile(configPath, "utf-8") - .then((rawData) => { - const config: DockerConfig = JSON.parse(rawData); - const hostIndex = config.hosts.findIndex( - (host) => host.name === hostName, - ); - - if (hostIndex === -1) { - res.status(404).json({ error: "Host not found." }); - return; - } - - config.hosts.splice(hostIndex, 1); - - return fs.promises - .writeFile(configPath, JSON.stringify(config, null, 2)) - .then(() => { - logger.info(`Removed host: ${hostName}`); - res.status(200).json({ message: "Host removed successfully." }); - }); - }) - .catch((error) => { - logger.error("Error removing host: " + (error as Error).message); - res.status(500).json({ error: "Failed to remove host." }); - }); + const ConfHandler = createConfHandler(req, res); + return ConfHandler.addHost(req); }); export default router; diff --git a/src/typings/dockerConfig.ts b/src/typings/dockerConfig.ts index fea0f4ec..26d72951 100644 --- a/src/typings/dockerConfig.ts +++ b/src/typings/dockerConfig.ts @@ -7,4 +7,5 @@ interface target { interface dockerConfig { hosts: target[]; } + export { dockerConfig, target }; diff --git a/src/typings/syncRequestBody.ts b/src/typings/syncRequestBody.ts new file mode 100644 index 00000000..36fd70a4 --- /dev/null +++ b/src/typings/syncRequestBody.ts @@ -0,0 +1,5 @@ +interface SyncRequestBody { + files: Record; +} + +export { SyncRequestBody }; diff --git a/src/typings/table.ts b/src/typings/table.ts index 4845ebaa..cf0c18ab 100644 --- a/src/typings/table.ts +++ b/src/typings/table.ts @@ -4,4 +4,8 @@ type Table = { timestamp: string; // ISO 8601 formatted datetime string }; -export default Table; +interface DataRow { + info: string; +} + +export { Table, DataRow }; diff --git a/src/typings/template.ts b/src/typings/template.ts new file mode 100644 index 00000000..71e0c8a3 --- /dev/null +++ b/src/typings/template.ts @@ -0,0 +1,5 @@ +interface TemplateData { + text: string; +} + +export { TemplateData }; From c5e4e6c14f447fdb392658960a215ee7af8d6d0d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 19:03:26 +0100 Subject: [PATCH 095/369] Chore: Moving some typings around Fix: Changing to atomic write = no more file system race coditions --- src/config/hostsystem.ts | 3 +- src/config/initFiles.ts | 5 +- src/config/loggerConfig.ts | 63 ------ src/controllers/fetchData.ts | 3 +- src/controllers/highAvailability.ts | 24 +-- src/misc/dependencyGraphs/mermaid-all.txt | 225 +++++++++++++--------- src/typings/atomicWrite.ts | 6 + src/typings/dockerConfig.ts | 26 ++- src/typings/ha.ts | 20 ++ src/typings/hostData.ts | 26 +++ src/typings/response.ts | 6 + src/utils/atomicWrite.ts | 35 ++++ src/utils/connectionChecker.ts | 21 +- src/utils/containerService.ts | 32 +-- src/utils/dockerClient.ts | 17 +- src/utils/extractHostData.ts | 25 +-- src/utils/logger.ts | 3 +- 17 files changed, 273 insertions(+), 267 deletions(-) delete mode 100644 src/config/loggerConfig.ts create mode 100644 src/typings/atomicWrite.ts create mode 100644 src/typings/ha.ts create mode 100644 src/typings/hostData.ts create mode 100644 src/typings/response.ts create mode 100644 src/utils/atomicWrite.ts diff --git a/src/config/hostsystem.ts b/src/config/hostsystem.ts index 91e44ed5..e9c04de3 100644 --- a/src/config/hostsystem.ts +++ b/src/config/hostsystem.ts @@ -2,6 +2,7 @@ import { RUNNING_IN_DOCKER, VERSION } from "./variables"; import fs from "fs"; import logger from "../utils/logger"; import os from "os"; +import { atomicWrite } from "../utils/atomicWrite"; const userConf = "./src/data/user.conf"; const inDocker: boolean = RUNNING_IN_DOCKER == "true"; @@ -44,7 +45,7 @@ function writeUserConf() { } if (shouldRewriteConfig) { - fs.writeFileSync(userConf, JSON.stringify(installationDetails, null, 2)); + atomicWrite(userConf, JSON.stringify(installationDetails, null, 2)); logger.debug("Configuration file created/updated:", userConf); } diff --git a/src/config/initFiles.ts b/src/config/initFiles.ts index 79822661..008749cb 100644 --- a/src/config/initFiles.ts +++ b/src/config/initFiles.ts @@ -1,5 +1,6 @@ -import { writeFileSync, existsSync } from "fs"; +import { existsSync } from "fs"; import logger from "../utils/logger"; +import { atomicWrite } from "../utils/atomicWrite"; const files = [ { @@ -29,7 +30,7 @@ const files = [ function initFiles(): void { files.forEach(({ path: filePath, content }) => { if (!existsSync(filePath)) { - writeFileSync(filePath, content); + atomicWrite(filePath, content); logger.info(`Created: ${filePath}`); } else { logger.debug(`Skipped (already exists): ${filePath}`); diff --git a/src/config/loggerConfig.ts b/src/config/loggerConfig.ts deleted file mode 100644 index f2b30f43..00000000 --- a/src/config/loggerConfig.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { createLogger, format, transports } from "winston"; -import DailyRotateFile from "winston-daily-rotate-file"; - -const gray = "\x1b[90m"; -const reset = "\x1b[0m"; -const white = "\x1b[97m"; -const red = "\x1b[31m"; -const green = "\x1b[32m"; -const yellow = "\x1b[33m"; -const blue = "\x1b[34m"; - -const ignoreExitListenerLogs = format((info) => { - if ( - typeof info.message === "string" && - info.message.includes("Exit listeners detected") - ) { - return false; // Silences annoying logs - } - return info; -}); - -function colorLog(level: string, levelName: string) { - switch (level) { - case "info": - return `${green}${levelName}${reset}`; - case "debug": - return `${blue}${levelName}${reset}`; - case "error": - return `${red}${levelName}${reset}`; - case "warn": - return `${yellow}${levelName}${reset}`; - default: - return `${gray}UNKNOWN${reset}`; - } -} - -const logger = createLogger({ - level: "debug", - format: format.combine( - ignoreExitListenerLogs(), - format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), - format.printf((info) => { - const level = info.level.toUpperCase().padEnd(5, " "); - const timestamp = `${gray}${info.timestamp}${reset}`; - const levelColorized = colorLog(info.level.toLowerCase(), level); - const message = `${white}${info.message}${reset}`; - - return `${timestamp} ${levelColorized} : ${message}`; - }), - ), - transports: [ - new transports.Console(), - new DailyRotateFile({ - filename: "logs/app-%DATE%.log", - datePattern: "YYYY-MM-DD", - maxSize: "20m", - maxFiles: "14d", - zippedArchive: true, - }), - ], -}); - -export default logger; diff --git a/src/controllers/fetchData.ts b/src/controllers/fetchData.ts index dfc24878..07438ec2 100644 --- a/src/controllers/fetchData.ts +++ b/src/controllers/fetchData.ts @@ -2,6 +2,7 @@ import db from "../config/db"; import fetchAllContainers from "../utils/containerService"; import logger from "../utils/logger"; import fs from "fs"; +import { atomicWrite } from "../utils/atomicWrite"; const filePath = "./src/data/states.json"; let previousState: { [key: string]: string } = {}; @@ -60,7 +61,7 @@ const fetchData = async (): Promise => { // Compare previous and current state if (JSON.stringify(previousState) !== JSON.stringify(containerStatus)) { - fs.writeFileSync(filePath, JSON.stringify(containerStatus, null, 2)); + atomicWrite(filePath, JSON.stringify(containerStatus, null, 2)); logger.info(`Container states saved to ${filePath}`); // TODO: Add logic + notification levels per service } else { diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index c5e3325a..1b28b4f7 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -9,28 +9,11 @@ import { HA_MASTER_IP, HA_NODE, } from "../config/variables"; +import { atomicWrite } from "../utils/atomicWrite"; +import { HighAvailabilityConfig, HaNodeConfig, NodeCache } from "../typings/ha"; const sleep = promisify(setTimeout); -interface HighAvailabilityConfig { - active: boolean; - master: boolean; - nodes: string[]; -} - -interface Node { - ip: string; - id: number; -} - -interface HaNodeConfig { - master: string; -} - -interface NodeCache { - [nodes: string]: Node; -} - const haMasterPath: string = "./src/data/highAvailability.json"; const haNodePath: string = "./src/data/haNode.json"; const nodeCachePath: string = "./src/data/nodeCache.json"; @@ -61,7 +44,6 @@ async function acquireLock(): Promise { } const backoffMs = BASE_DELAY_MS * Math.pow(2, retryCount); - // Add jitter to prevent thundering herd const jitter = Math.random() * 0.3 * backoffMs; const delayMs = backoffMs + jitter; @@ -73,7 +55,7 @@ async function acquireLock(): Promise { } try { - await fs.promises.writeFile(lockFilePath, "locked", { flag: "wx" }); + atomicWrite(lockFilePath, "locked", { exclusive: true }); logger.debug("Lock acquired."); } catch (error) { logger.error(`Error acquiring lock: ${(error as Error).message}`); diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt index e81fdf84..e61282b8 100644 --- a/src/misc/dependencyGraphs/mermaid-all.txt +++ b/src/misc/dependencyGraphs/mermaid-all.txt @@ -3,125 +3,160 @@ flowchart LR 0["server.ts"] subgraph 1["controllers"] 2["highAvailability.ts"] -A["proxy.ts"] -B["scheduler.ts"] -D["fetchData.ts"] -T["frontendConfiguration.ts"] +E["proxy.ts"] +F["scheduler.ts"] +H["fetchData.ts"] +V["auth.ts"] +12["frontendConfiguration.ts"] end 3["util"] subgraph 4["config"] 5["variables.ts"] -9["initFiles.ts"] -C["db.ts"] -1F["swaggerConfig.ts"] +D["initFiles.ts"] +G["db.ts"] +1R["swaggerConfig.ts"] end subgraph 6["data"] 7["variables.json"] end -8["init.ts"] -subgraph E["utils"] -F["containerService.ts"] -G["dockerClient.ts"] -J["rateLimitFS.ts"] -W["connectionChecker.ts"] -X["writeOfflineLog.ts"] -subgraph 12["notifications"] -13["_notify.ts"] -14["discord.ts"] -15["_template.ts"] -16["email.ts"] -17["pushbullet.ts"] -18["pushover.ts"] -19["slack.ts"] -1A["telegram.ts"] -1B["whatsapp.ts"] +subgraph 8["typings"] +9["ha.ts"] end -1E["swaggerDocs.ts"] +subgraph A["utils"] +B["atomicWrite.ts"] +I["containerService.ts"] +J["dockerClient.ts"] +O["rateLimitFS.ts"] +16["connectionChecker.ts"] +subgraph 1D["notifications"] +1E["_notify.ts"] +1F["discord.ts"] +1G["_template.ts"] +1H["email.ts"] +1I["pushbullet.ts"] +1J["pushover.ts"] +1K["slack.ts"] +1L["telegram.ts"] +1M["whatsapp.ts"] end -subgraph H["middleware"] -I["authMiddleware.ts"] -K["checkLock.ts"] -L["rateLimiter.ts"] +1Q["swaggerDocs.ts"] end -subgraph M["routes"] -subgraph N["auth"] -O["routes.ts"] +C["init.ts"] +subgraph K["middleware"] +L["authMiddleware.ts"] +P["checkLock.ts"] +Q["rateLimiter.ts"] end -subgraph P["data"] -Q["routes.ts"] +subgraph M["handlers"] +N["response.ts"] +U["auth.ts"] +Y["data.ts"] +11["frontend.ts"] +15["api.ts"] +19["ha.ts"] +1C["notification.ts"] +1P["conf.ts"] end -subgraph R["frontendController"] -S["routes.ts"] +subgraph R["routes"] +subgraph S["auth"] +T["routes.ts"] end -subgraph U["getter"] -V["routes.ts"] +subgraph W["data"] +X["routes.ts"] end -subgraph Y["highavailability"] -Z["routes.ts"] +subgraph Z["frontendController"] +10["routes.ts"] end -subgraph 10["notifications"] -11["routes.ts"] +subgraph 13["getter"] +14["routes.ts"] end -subgraph 1C["setter"] -1D["routes.ts"] +subgraph 17["highavailability"] +18["routes.ts"] +end +subgraph 1A["notifications"] +1B["routes.ts"] +end +subgraph 1N["setter"] +1O["routes.ts"] end end 0-->2 -0-->8 +0-->C 2-->5 +2-->9 +2-->B 2-->3 5-->7 -8-->9 -8-->A -8-->B -8-->I -8-->K -8-->L -8-->O -8-->Q -8-->S -8-->V -8-->Z -8-->11 -8-->1D -8-->1E -A-->5 -B-->C -B-->D -D-->C -D-->F +C-->D +C-->E +C-->F +C-->L +C-->P +C-->Q +C-->T +C-->X +C-->10 +C-->14 +C-->18 +C-->1B +C-->1O +C-->1Q +D-->B +E-->5 F-->G +F-->H +H-->G +H-->B +H-->I +I-->B I-->J -K-->J -Q-->C -S-->T -V-->B -V-->W -V-->F -V-->G -V-->X -Z-->2 -11-->13 -13-->14 -13-->16 -13-->17 -13-->18 -13-->19 -13-->1A -13-->1B -14-->5 +L-->N +L-->O +P-->N +P-->O +T-->U +U-->V +U-->N +X-->Y +Y-->G +Y-->N +10-->11 +11-->12 +11-->N 14-->15 -16-->5 -16-->15 -17-->5 -17-->15 -18-->5 -18-->15 -19-->5 -19-->15 -1A-->5 -1A-->15 -1B-->5 -1B-->15 -1D-->B +15-->F +15-->16 +15-->I +15-->J +15-->N +18-->19 +19-->2 +19-->N +1B-->1C +1C-->1E +1C-->N 1E-->1F +1E-->1H +1E-->1I +1E-->1J +1E-->1K +1E-->1L +1E-->1M +1F-->5 +1F-->1G +1H-->5 +1H-->1G +1I-->5 +1I-->1G +1J-->5 +1J-->1G +1K-->5 +1K-->1G +1L-->5 +1L-->1G +1M-->5 +1M-->1G +1O-->1P +1P-->F +1P-->N +1Q-->1R diff --git a/src/typings/atomicWrite.ts b/src/typings/atomicWrite.ts new file mode 100644 index 00000000..1f4bfb4a --- /dev/null +++ b/src/typings/atomicWrite.ts @@ -0,0 +1,6 @@ +interface AtomicWriteOptions { + mode?: number; + exclusive?: boolean; +} + +export { AtomicWriteOptions }; diff --git a/src/typings/dockerConfig.ts b/src/typings/dockerConfig.ts index 26d72951..a1749d1f 100644 --- a/src/typings/dockerConfig.ts +++ b/src/typings/dockerConfig.ts @@ -8,4 +8,28 @@ interface dockerConfig { hosts: target[]; } -export { dockerConfig, target }; +interface HostConfig { + name: string; + [key: string]: string | number; +} + +interface ContainerData { + name: string; + id: string; + hostName: string; + state: string; + cpu_usage: number; + mem_usage: number; + mem_limit: number; + net_rx: number; + net_tx: number; + current_net_rx: number; + current_net_tx: number; + networkMode: string; +} + +interface AllContainerData { + [hostName: string]: ContainerData[] | { error: string }; +} + +export { dockerConfig, target, ContainerData, AllContainerData, HostConfig }; diff --git a/src/typings/ha.ts b/src/typings/ha.ts new file mode 100644 index 00000000..a722fff8 --- /dev/null +++ b/src/typings/ha.ts @@ -0,0 +1,20 @@ +interface HighAvailabilityConfig { + active: boolean; + master: boolean; + nodes: string[]; +} + +interface Node { + ip: string; + id: number; +} + +interface HaNodeConfig { + master: string; +} + +interface NodeCache { + [nodes: string]: Node; +} + +export { HighAvailabilityConfig, Node, HaNodeConfig, NodeCache }; diff --git a/src/typings/hostData.ts b/src/typings/hostData.ts new file mode 100644 index 00000000..cf5a78da --- /dev/null +++ b/src/typings/hostData.ts @@ -0,0 +1,26 @@ +interface Component { + Name: string; + Version: string; +} + +interface JsonData { + hostName: string; + info: { + ID: string; + Containers: number; + ContainersRunning: number; + ContainersPaused: number; + ContainersStopped: number; + Images: number; + OperatingSystem: string; + KernelVersion: string; + Architecture: string; + MemTotal: number; + NCPU: number; + }; + version: { + Components: Component[]; + }; +} + +export { JsonData }; diff --git a/src/typings/response.ts b/src/typings/response.ts new file mode 100644 index 00000000..b122dfe2 --- /dev/null +++ b/src/typings/response.ts @@ -0,0 +1,6 @@ +interface StatusResponse { + ApiReachable: boolean; + online: { [key: string]: boolean }; +} + +export { StatusResponse }; diff --git a/src/utils/atomicWrite.ts b/src/utils/atomicWrite.ts new file mode 100644 index 00000000..51f33759 --- /dev/null +++ b/src/utils/atomicWrite.ts @@ -0,0 +1,35 @@ +import fs from "fs"; +import logger from "./logger"; +import { AtomicWriteOptions } from "../typings/atomicWrite"; + +export function atomicWrite( + targetPath: string, + data: string | Buffer | Record, + options: AtomicWriteOptions = {}, +): void { + const { mode = 0o600, exclusive = false } = options; + const tempFile = `${targetPath}.tmp`; + + try { + const writeData = + typeof data === "object" && !(data instanceof Buffer) + ? JSON.stringify(data, null, 2) + : data; + + if (exclusive && fs.existsSync(targetPath)) { + throw new Error(`File already exists: ${targetPath}`); + } + + fs.writeFileSync(tempFile, writeData, { mode }); + + fs.renameSync(tempFile, targetPath); + + logger.debug(`File successfully written to: ${targetPath}`); + } catch (error: unknown) { + if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); + logger.error( + `Failed to write file at ${targetPath}: ${(error as Error).message}`, + ); + throw error; + } +} diff --git a/src/utils/connectionChecker.ts b/src/utils/connectionChecker.ts index 92efba3d..85b00dde 100644 --- a/src/utils/connectionChecker.ts +++ b/src/utils/connectionChecker.ts @@ -1,26 +1,17 @@ import * as fs from "fs"; import * as net from "net"; -import logger from "../config/loggerConfig"; +import logger from "./logger"; +import { target } from "../typings/dockerConfig"; +import { StatusResponse } from "../typings/response"; const filePath: string = "./src/data/dockerConfig.json"; -interface Host { - name: string; - url: string; - port: string; -} - -interface StatusResponse { - ApiReachable: boolean; - online: { [key: string]: boolean }; -} - -async function checkHostStatus(hosts: Host[]): Promise { +async function checkHostStatus(hosts: target[]): Promise { const results: { [key: string]: boolean } = {}; for (const host of hosts) { const { name, url, port } = host; - const isOnline = await checkPort(url, parseInt(port, 10)); + const isOnline = await checkPort(url, port); results[name] = !!isOnline; @@ -65,7 +56,7 @@ async function checkReachability(): Promise { try { const data = fs.readFileSync(filePath, "utf-8"); const parsedData = JSON.parse(data); - const hosts: Host[] = parsedData.hosts; + const hosts: target[] = parsedData.hosts; return await checkHostStatus(hosts); } catch (error: unknown) { logger.error(`Error reading file: ${error as Error}`); diff --git a/src/utils/containerService.ts b/src/utils/containerService.ts index 841e9c2b..f9277c1a 100644 --- a/src/utils/containerService.ts +++ b/src/utils/containerService.ts @@ -2,31 +2,9 @@ import logger from "./logger"; import { ContainerInfo, ContainerStats, ContainerInspectInfo } from "dockerode"; import getDockerClient from "./dockerClient"; import fs from "fs"; +import { atomicWrite } from "./atomicWrite"; const configPath = "./src/data/dockerConfig.json"; - -interface HostConfig { - name: string; - [key: string]: string | number; -} - -interface ContainerData { - name: string; - id: string; - hostName: string; - state: string; - cpu_usage: number; - mem_usage: number; - mem_limit: number; - net_rx: number; - net_tx: number; - current_net_rx: number; - current_net_tx: number; - networkMode: string; -} - -interface AllContainerData { - [hostName: string]: ContainerData[] | { error: string }; -} +import { AllContainerData, HostConfig } from "../typings/dockerConfig"; function loadConfig() { try { @@ -34,11 +12,7 @@ function loadConfig() { logger.warn( `Config file not found. Creating an empty file at ${configPath}`, ); - fs.writeFileSync( - configPath, - JSON.stringify({ hosts: [] }, null, 2), - "utf-8", - ); + atomicWrite(configPath, JSON.stringify({ hosts: [] }, null, 2)); } const configData = fs.readFileSync(configPath, "utf-8"); diff --git a/src/utils/dockerClient.ts b/src/utils/dockerClient.ts index dc0f5e91..8f2718b2 100644 --- a/src/utils/dockerClient.ts +++ b/src/utils/dockerClient.ts @@ -1,24 +1,15 @@ -// src/utils/dockerClient.ts import Docker from "dockerode"; import fs from "fs"; import logger from "./logger"; -interface DockerHostConfig { - name: string; - url: string; - port?: number; -} - -interface DockerConfig { - hosts: DockerHostConfig[]; -} +import { dockerConfig, target } from "../typings/dockerConfig"; -function loadDockerConfig(): DockerConfig { +function loadDockerConfig(): dockerConfig { const configPath = "./src/data/dockerConfig.json"; try { const rawData = fs.readFileSync(configPath, "utf-8"); logger.debug("Refreshed DockerConfig.json"); - return JSON.parse(rawData) as DockerConfig; + return JSON.parse(rawData) as dockerConfig; } catch (error: unknown) { logger.error( "Error loading dockerConfig.json: " + (error as Error).message, @@ -27,7 +18,7 @@ function loadDockerConfig(): DockerConfig { } } -function createDockerClient(hostConfig: DockerHostConfig): Docker { +function createDockerClient(hostConfig: target): Docker { logger.info( `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port || 2375}`, ); diff --git a/src/utils/extractHostData.ts b/src/utils/extractHostData.ts index 25ea0168..0af612ec 100644 --- a/src/utils/extractHostData.ts +++ b/src/utils/extractHostData.ts @@ -1,27 +1,4 @@ -interface Component { - Name: string; - Version: string; -} - -interface JsonData { - hostName: string; - info: { - ID: string; - Containers: number; - ContainersRunning: number; - ContainersPaused: number; - ContainersStopped: number; - Images: number; - OperatingSystem: string; - KernelVersion: string; - Architecture: string; - MemTotal: number; - NCPU: number; - }; - version: { - Components: Component[]; - }; -} +import { JsonData } from "../typings/hostData"; type ComponentMap = Record; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 8c1ea4a0..d1a3e85a 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,6 +1,5 @@ import { createLogger, format, transports } from "winston"; import DailyRotateFile from "winston-daily-rotate-file"; -import loggerConfig from "../config/loggerConfig"; // ANSI color codes for log level customization const colors = { @@ -42,7 +41,7 @@ const filterLogs = format((info) => { // Logger instance const logger = createLogger({ - level: loggerConfig.level || "debug", + level: "debug", format: format.combine( filterLogs(), format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), From 604a6cf4b9c765190847fc8b6e512266d2c0454d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 20:53:29 +0100 Subject: [PATCH 096/369] Chore: Move files around --- README.md | 13 +- package.json | 4 +- .../dependencyGraphs}/.dependency-cruiser.cjs | 0 .../dependencyGraphs/createDependencyGraph.sh | 41 ++++ src/misc/dependencyGraphs/mermaid-all.txt | 217 +++++++----------- src/misc/dependencyGraphs/mermaid-api.txt | 44 ++-- src/misc/dependencyGraphs/mermaid-auth.txt | 21 +- src/misc/dependencyGraphs/mermaid-conf.txt | 36 +-- src/misc/dependencyGraphs/mermaid-data.txt | 22 +- .../dependencyGraphs/mermaid-frontend.txt | 22 +- src/misc/dependencyGraphs/mermaid-ha.txt | 34 ++- .../mermaid-notificationService.txt | 38 +-- src/{utils => misc}/removeUnusedDeps.sh | 0 src/utils/createDependencyGraph.sh | 38 --- 14 files changed, 253 insertions(+), 277 deletions(-) rename src/{ => misc/dependencyGraphs}/.dependency-cruiser.cjs (100%) create mode 100755 src/misc/dependencyGraphs/createDependencyGraph.sh rename src/{utils => misc}/removeUnusedDeps.sh (100%) delete mode 100755 src/utils/createDependencyGraph.sh diff --git a/README.md b/README.md index 4e6daf38..f8e330cd 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,15 @@ ![Dockstat Logo](.github/DockStat.png) -_Pipelines:_
-[![Docker Image CI](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml/badge.svg?branch=main)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml)
-[![Build dockstatapi:nightly](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml)
-[![Tests](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml/badge.svg?branch=dev)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) +

+ +# Pipelines + +[![Docker Image CI](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/build-image.yml?branch=main&label=Docker%20Image%20CI&style=for-the-badge&logo=docker)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml) +[![Build dockstatapi:nightly](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/build-dev.yaml?branch=dev&label=Nightly%20Build&style=for-the-badge&logo=github)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml) +[![Validate](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/validation.yml?branch=dev&label=Validation&style=for-the-badge&logo=checkmarx)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) + +
This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. With this new release a couple of extra features (compared to v1) are going to be available. diff --git a/package.json b/package.json index 6dd43aba..29e307e6 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "start:build": "npx tsc && node dist/server.js", "dev": "npm run local-env-file && nodemon", "dev:trace": "npm run local-env-file && nodemon --trace-uncaught --trace-warnings", - "dep": "bash ./src/utils/createDependencyGraph.sh", - "dep:remove": "bash ./src/utils/removeUnusedDeps.sh && npm run dep", + "dep": "bash ./src/misc/dependencyGraphs/createDependencyGraph.sh", + "dep:remove": "bash ./src/misc/removeUnusedDeps.sh && npm run dep", "build": "npx tsc", "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", "mini": "bash ./src/misc/minifyDist.sh", diff --git a/src/.dependency-cruiser.cjs b/src/misc/dependencyGraphs/.dependency-cruiser.cjs similarity index 100% rename from src/.dependency-cruiser.cjs rename to src/misc/dependencyGraphs/.dependency-cruiser.cjs diff --git a/src/misc/dependencyGraphs/createDependencyGraph.sh b/src/misc/dependencyGraphs/createDependencyGraph.sh new file mode 100755 index 00000000..4e118194 --- /dev/null +++ b/src/misc/dependencyGraphs/createDependencyGraph.sh @@ -0,0 +1,41 @@ +#!/bin/bash +TMP=$(mktemp) +IGNORE="node_modules|logger|.dependency-cruiser|path|fs|os|https|net|process|util" + +cat ./src/init.ts | grep "./routes" | awk '{print $2,$4}' > $TMP + +spawn_worker(){ + local line="$1" + local target_route="$(echo "$line" | cut -d '"' -f2 | sed 's|^./routes|./src/routes|').ts" + local route=$(echo "$line" | awk '{print $1}') + + echo -e "\nRoute: $route \n${target_route}" + + npx depcruise \ + -c ./src/misc/dependencyGraphs/.dependency-cruiser.cjs \ + -p cli-feedback \ + -T mermaid \ + -x "$IGNORE" \ + -f ./src/misc/dependencyGraphs/mermaid-${route}.txt \ + ${target_route} || exit 1 +} + +while read line; do + spawn_worker "$line" & +done < <(cat $TMP) + +npx depcruise \ + -c ./src/misc/dependencyGraphs/.dependency-cruiser.cjs \ + -p cli-feedback \ + -T mermaid \ + -x "$IGNORE" \ + -f ./src/misc/dependencyGraphs/mermaid-all.txt \ + ./src/server.ts || exit 1 + +wait + +find ./src/misc/dependencyGraphs -type f -name "*.txt" -exec sed -i 's/flowchart LR/flowchart TB/g' {} + + +echo -e "\n========\n\n DONE\n\n========" + +exit 0 diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt index e61282b8..ad02a822 100644 --- a/src/misc/dependencyGraphs/mermaid-all.txt +++ b/src/misc/dependencyGraphs/mermaid-all.txt @@ -1,20 +1,19 @@ -flowchart LR +flowchart TB -0["server.ts"] -subgraph 1["controllers"] -2["highAvailability.ts"] -E["proxy.ts"] -F["scheduler.ts"] -H["fetchData.ts"] -V["auth.ts"] -12["frontendConfiguration.ts"] +subgraph 0["src"] +1["server.ts"] +subgraph 2["controllers"] +3["highAvailability.ts"] +C["proxy.ts"] +D["scheduler.ts"] +F["fetchData.ts"] +Q["auth.ts"] +X["frontendConfiguration.ts"] end -3["util"] subgraph 4["config"] 5["variables.ts"] -D["initFiles.ts"] -G["db.ts"] -1R["swaggerConfig.ts"] +B["initFiles.ts"] +E["db.ts"] end subgraph 6["data"] 7["variables.json"] @@ -22,141 +21,87 @@ end subgraph 8["typings"] 9["ha.ts"] end -subgraph A["utils"] -B["atomicWrite.ts"] -I["containerService.ts"] -J["dockerClient.ts"] -O["rateLimitFS.ts"] -16["connectionChecker.ts"] -subgraph 1D["notifications"] -1E["_notify.ts"] -1F["discord.ts"] -1G["_template.ts"] -1H["email.ts"] -1I["pushbullet.ts"] -1J["pushover.ts"] -1K["slack.ts"] -1L["telegram.ts"] -1M["whatsapp.ts"] +A["init.ts"] +subgraph G["middleware"] +H["authMiddleware.ts"] +K["checkLock.ts"] +L["rateLimiter.ts"] end -1Q["swaggerDocs.ts"] +subgraph I["handlers"] +J["response.ts"] +P["auth.ts"] +T["data.ts"] +W["frontend.ts"] +10["api.ts"] +13["ha.ts"] +16["notification.ts"] +19["conf.ts"] end -C["init.ts"] -subgraph K["middleware"] -L["authMiddleware.ts"] -P["checkLock.ts"] -Q["rateLimiter.ts"] +subgraph M["routes"] +subgraph N["auth"] +O["routes.ts"] end -subgraph M["handlers"] -N["response.ts"] -U["auth.ts"] -Y["data.ts"] -11["frontend.ts"] -15["api.ts"] -19["ha.ts"] -1C["notification.ts"] -1P["conf.ts"] +subgraph R["data"] +S["routes.ts"] end -subgraph R["routes"] -subgraph S["auth"] -T["routes.ts"] +subgraph U["frontendController"] +V["routes.ts"] end -subgraph W["data"] -X["routes.ts"] +subgraph Y["getter"] +Z["routes.ts"] end -subgraph Z["frontendController"] -10["routes.ts"] +subgraph 11["highavailability"] +12["routes.ts"] end -subgraph 13["getter"] -14["routes.ts"] +subgraph 14["notifications"] +15["routes.ts"] end -subgraph 17["highavailability"] +subgraph 17["setter"] 18["routes.ts"] end -subgraph 1A["notifications"] -1B["routes.ts"] end -subgraph 1N["setter"] -1O["routes.ts"] end -end -0-->2 -0-->C -2-->5 -2-->9 -2-->B -2-->3 +1-->3 +1-->A +3-->5 +3-->9 5-->7 -C-->D -C-->E -C-->F -C-->L -C-->P -C-->Q -C-->T -C-->X -C-->10 -C-->14 -C-->18 -C-->1B -C-->1O -C-->1Q -D-->B -E-->5 -F-->G -F-->H -H-->G -H-->B -H-->I -I-->B -I-->J -L-->N -L-->O -P-->N -P-->O -T-->U -U-->V -U-->N -X-->Y -Y-->G -Y-->N -10-->11 -11-->12 -11-->N -14-->15 -15-->F +A-->B +A-->C +A-->D +A-->H +A-->K +A-->L +A-->O +A-->S +A-->V +A-->Z +A-->12 +A-->15 +A-->18 +C-->5 +D-->E +D-->F +F-->E +H-->J +K-->J +O-->P +P-->Q +P-->J +S-->T +T-->E +T-->J +V-->W +W-->X +W-->J +Z-->10 +10-->D +10-->J +12-->13 +13-->3 +13-->J 15-->16 -15-->I -15-->J -15-->N +16-->J 18-->19 -19-->2 -19-->N -1B-->1C -1C-->1E -1C-->N -1E-->1F -1E-->1H -1E-->1I -1E-->1J -1E-->1K -1E-->1L -1E-->1M -1F-->5 -1F-->1G -1H-->5 -1H-->1G -1I-->5 -1I-->1G -1J-->5 -1J-->1G -1K-->5 -1K-->1G -1L-->5 -1L-->1G -1M-->5 -1M-->1G -1O-->1P -1P-->F -1P-->N -1Q-->1R +19-->D +19-->J diff --git a/src/misc/dependencyGraphs/mermaid-api.txt b/src/misc/dependencyGraphs/mermaid-api.txt index e7c85cc8..3cb4811e 100644 --- a/src/misc/dependencyGraphs/mermaid-api.txt +++ b/src/misc/dependencyGraphs/mermaid-api.txt @@ -1,32 +1,26 @@ -flowchart LR +flowchart TB -subgraph 0["routes"] -subgraph 1["getter"] -2["routes.ts"] +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["getter"] +3["routes.ts"] end end -subgraph 3["controllers"] -4["scheduler.ts"] -7["fetchData.ts"] +subgraph 4["handlers"] +5["api.ts"] +B["response.ts"] end -subgraph 5["config"] -6["db.ts"] +subgraph 6["controllers"] +7["scheduler.ts"] +A["fetchData.ts"] end -subgraph 8["utils"] -9["containerService.ts"] -A["dockerClient.ts"] -B["connectionChecker.ts"] -C["extractHostData.ts"] -D["writeOfflineLog.ts"] +subgraph 8["config"] +9["db.ts"] end -2-->4 -2-->B -2-->9 -2-->A -2-->C -2-->D -4-->6 -4-->7 -7-->6 +end +3-->5 +5-->7 +5-->B 7-->9 -9-->A +7-->A +A-->9 diff --git a/src/misc/dependencyGraphs/mermaid-auth.txt b/src/misc/dependencyGraphs/mermaid-auth.txt index aaeb683b..336ddedb 100644 --- a/src/misc/dependencyGraphs/mermaid-auth.txt +++ b/src/misc/dependencyGraphs/mermaid-auth.txt @@ -1,8 +1,19 @@ -flowchart LR +flowchart TB -subgraph 0["routes"] -subgraph 1["auth"] -2["routes.ts"] +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["auth"] +3["routes.ts"] end end - +subgraph 4["handlers"] +5["auth.ts"] +8["response.ts"] +end +subgraph 6["controllers"] +7["auth.ts"] +end +end +3-->5 +5-->7 +5-->8 diff --git a/src/misc/dependencyGraphs/mermaid-conf.txt b/src/misc/dependencyGraphs/mermaid-conf.txt index ba9ca669..370dd892 100644 --- a/src/misc/dependencyGraphs/mermaid-conf.txt +++ b/src/misc/dependencyGraphs/mermaid-conf.txt @@ -1,24 +1,26 @@ -flowchart LR +flowchart TB -subgraph 0["routes"] -subgraph 1["setter"] -2["routes.ts"] +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["setter"] +3["routes.ts"] end end -subgraph 3["controllers"] -4["scheduler.ts"] -7["fetchData.ts"] +subgraph 4["handlers"] +5["conf.ts"] +B["response.ts"] end -subgraph 5["config"] -6["db.ts"] +subgraph 6["controllers"] +7["scheduler.ts"] +A["fetchData.ts"] end -subgraph 8["utils"] -9["containerService.ts"] -A["dockerClient.ts"] +subgraph 8["config"] +9["db.ts"] end -2-->4 -4-->6 -4-->7 -7-->6 +end +3-->5 +5-->7 +5-->B 7-->9 -9-->A +7-->A +A-->9 diff --git a/src/misc/dependencyGraphs/mermaid-data.txt b/src/misc/dependencyGraphs/mermaid-data.txt index 107d46af..4aa6a133 100644 --- a/src/misc/dependencyGraphs/mermaid-data.txt +++ b/src/misc/dependencyGraphs/mermaid-data.txt @@ -1,11 +1,19 @@ -flowchart LR +flowchart TB -subgraph 0["routes"] -subgraph 1["data"] -2["routes.ts"] +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["data"] +3["routes.ts"] end end -subgraph 3["config"] -4["db.ts"] +subgraph 4["handlers"] +5["data.ts"] +8["response.ts"] end -2-->4 +subgraph 6["config"] +7["db.ts"] +end +end +3-->5 +5-->7 +5-->8 diff --git a/src/misc/dependencyGraphs/mermaid-frontend.txt b/src/misc/dependencyGraphs/mermaid-frontend.txt index 03340053..8dde5ce9 100644 --- a/src/misc/dependencyGraphs/mermaid-frontend.txt +++ b/src/misc/dependencyGraphs/mermaid-frontend.txt @@ -1,11 +1,19 @@ -flowchart LR +flowchart TB -subgraph 0["routes"] -subgraph 1["frontendController"] -2["routes.ts"] +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["frontendController"] +3["routes.ts"] end end -subgraph 3["controllers"] -4["frontendConfiguration.ts"] +subgraph 4["handlers"] +5["frontend.ts"] +8["response.ts"] end -2-->4 +subgraph 6["controllers"] +7["frontendConfiguration.ts"] +end +end +3-->5 +5-->7 +5-->8 diff --git a/src/misc/dependencyGraphs/mermaid-ha.txt b/src/misc/dependencyGraphs/mermaid-ha.txt index ce156053..2c789f6c 100644 --- a/src/misc/dependencyGraphs/mermaid-ha.txt +++ b/src/misc/dependencyGraphs/mermaid-ha.txt @@ -1,11 +1,31 @@ -flowchart LR +flowchart TB -subgraph 0["routes"] -subgraph 1["highavailability"] -2["routes.ts"] +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["highavailability"] +3["routes.ts"] end end -subgraph 3["controllers"] -4["highAvailability.ts"] +subgraph 4["handlers"] +5["ha.ts"] +E["response.ts"] end -2-->4 +subgraph 6["controllers"] +7["highAvailability.ts"] +end +subgraph 8["config"] +9["variables.ts"] +end +subgraph A["data"] +B["variables.json"] +end +subgraph C["typings"] +D["ha.ts"] +end +end +3-->5 +5-->7 +5-->E +7-->9 +7-->D +9-->B diff --git a/src/misc/dependencyGraphs/mermaid-notificationService.txt b/src/misc/dependencyGraphs/mermaid-notificationService.txt index cef6c2cd..2bc9731c 100644 --- a/src/misc/dependencyGraphs/mermaid-notificationService.txt +++ b/src/misc/dependencyGraphs/mermaid-notificationService.txt @@ -1,35 +1,15 @@ -flowchart LR +flowchart TB -subgraph 0["routes"] -subgraph 1["notifications"] -2["routes.ts"] +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["notifications"] +3["routes.ts"] end end -subgraph 3["utils"] -subgraph 4["notifications"] -5["_notify.ts"] -6["discord.ts"] -7["_template.ts"] -8["email.ts"] -9["pushbullet.ts"] -A["pushover.ts"] -B["slack.ts"] -C["telegram.ts"] -D["whatsapp.ts"] +subgraph 4["handlers"] +5["notification.ts"] +6["response.ts"] end end -2-->5 +3-->5 5-->6 -5-->8 -5-->9 -5-->A -5-->B -5-->C -5-->D -6-->7 -8-->7 -9-->7 -A-->7 -B-->7 -C-->7 -D-->7 diff --git a/src/utils/removeUnusedDeps.sh b/src/misc/removeUnusedDeps.sh similarity index 100% rename from src/utils/removeUnusedDeps.sh rename to src/misc/removeUnusedDeps.sh diff --git a/src/utils/createDependencyGraph.sh b/src/utils/createDependencyGraph.sh deleted file mode 100755 index 9c220f7a..00000000 --- a/src/utils/createDependencyGraph.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -cd src || exit 1 -TMP=$(mktemp) -IGNORE="../node_modules|logger|.dependency-cruiser|path|fs|os|https|net|process" - -cat ./server.ts | grep "./routes" | awk '{print $2,$4}' > $TMP - -spawn_worker(){ - local line="$1" - local target_route="$(echo "$line" | cut -d '"' -f2).ts" - local route=$(echo "$line" | awk '{print $1}') - - echo -e "\nRoute: $route \n${target_route}" - - npx depcruise \ - -p cli-feedback \ - -T mermaid \ - -x "$IGNORE" \ - -f ./misc/dependencyGraphs/mermaid-${route}.txt \ - ${target_route} || exit 1 -} - -while read line; do - spawn_worker "$line" & -done < <(cat $TMP) - -npx depcruise \ - -p cli-feedback \ - -T mermaid \ - -x "$IGNORE" \ - -f ./misc/dependencyGraphs/mermaid-all.txt \ - ./server.ts || exit 1 - -wait - -echo -e "\n========\n\n DONE\n\n========" - -exit 0 From dcf62b70cf38b757b28a3c8f2af741f00947c470 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 20:54:44 +0100 Subject: [PATCH 097/369] Chore: Update ReadMe --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index f8e330cd..8fc800a8 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ # Pipelines [![Docker Image CI](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/build-image.yml?branch=main&label=Docker%20Image%20CI&style=for-the-badge&logo=docker)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml) -[![Build dockstatapi:nightly](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/build-dev.yaml?branch=dev&label=Nightly%20Build&style=for-the-badge&logo=github)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-dev.yaml) -[![Validate](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/validation.yml?branch=dev&label=Validation&style=for-the-badge&logo=checkmarx)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) +[![Validation](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/validation.yml?branch=dev&label=Validation&style=for-the-badge&logo=checkmarx)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) From 45b0cfd8c58726c35321d4c89aa9a0610450dd64 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 1 Jan 2025 20:58:35 +0100 Subject: [PATCH 098/369] Fix: Remove escape characters from log file --- src/utils/logger.ts | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/utils/logger.ts b/src/utils/logger.ts index d1a3e85a..00adbdfc 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -45,23 +45,35 @@ const logger = createLogger({ format: format.combine( filterLogs(), format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), - format.printf((info) => { - const level = info.level.toUpperCase().padEnd(5, " "); - const timestamp = `${colors.gray}${info.timestamp}${colors.reset}`; - const levelColorized = colorizeLogLevel(info.level.toLowerCase(), level); - const message = `${colors.white}${info.message}${colors.reset}`; - - return `${timestamp} ${levelColorized} : ${message}`; - }), ), transports: [ - new transports.Console(), + new transports.Console({ + format: format.combine( + format.printf((info) => { + const level = info.level.toUpperCase().padEnd(5, " "); + const timestamp = `${colors.gray}${info.timestamp}${colors.reset}`; + const levelColorized = colorizeLogLevel( + info.level.toLowerCase(), + level, + ); + const message = `${colors.white}${info.message}${colors.reset}`; + + return `${timestamp} ${levelColorized} : ${message}`; + }), + ), + }), new DailyRotateFile({ filename: "logs/app-%DATE%.log", datePattern: "YYYY-MM-DD", maxSize: "20m", maxFiles: "14d", zippedArchive: true, + format: format.combine( + format.printf((info) => { + const level = info.level.toUpperCase().padEnd(5, " "); + return `${info.timestamp} ${level} : ${info.message}`; + }), + ), }), ], }); From ffb45521f031c00e96a804744342cc155af58ae2 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 2 Jan 2025 21:56:01 +0100 Subject: [PATCH 099/369] Chore: Cleaning up Dockerfile(s) --- .github/workflows/build-image.yaml | 2 + .github/workflows/validation.yaml | 16 +- .gitignore | 3 +- Dockerfile | 61 ----- Dockerfile-dev | 61 ----- TODO.md | 5 +- docker/Dockerfile-base | 59 +++++ docker/Dockerfile-dev | 59 +++++ .../docker-compose.yaml | 10 +- package-lock.json | 247 ++++++++++-------- package.json | 8 +- src/config/hostsystem.ts | 24 +- src/controllers/highAvailability.ts | 4 +- src/misc/.tmux.sh | 1 + src/server.ts | 5 - 15 files changed, 306 insertions(+), 259 deletions(-) delete mode 100644 Dockerfile delete mode 100644 Dockerfile-dev create mode 100644 docker/Dockerfile-base create mode 100644 docker/Dockerfile-dev rename docker-compose.yaml => docker/docker-compose.yaml (87%) create mode 100644 src/misc/.tmux.sh diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 720bed85..41f18cb8 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -39,6 +39,8 @@ jobs: uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64 + context: . + file: docker/Dockerfile-base push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index dfd9330a..2226171e 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -103,7 +103,7 @@ jobs: - uses: actions/checkout@v4 - name: Build the Container image - run: docker build . --file Dockerfile --tag localbuild/testimage:latest + run: docker build . --file docker/Dockerfile-base --tag localbuild/testimage:latest - name: Run Grype test run: grype -o sarif localbuild/testimage:latest > results.sarif @@ -126,16 +126,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Set up Node.js version from .nvmrc - run: | - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - nvm install - nvm use - node -v - npm -v - - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -160,6 +150,8 @@ jobs: - name: Build and Push Docker Images uses: docker/build-push-action@v6 with: + context: . + file: docker/Dockerfile-base platforms: linux/amd64,linux/arm64 push: false tags: ${{ steps.metadata.outputs.tags }} @@ -205,6 +197,8 @@ jobs: uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64, + context: . + file: docker/Dockerfile-dev push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} diff --git a/.gitignore b/.gitignore index 84449de1..dc93b889 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ # custom paths: src/data/* -docker +docker/master +docker/slave .test* # Created by https://www.toptal.com/developers/gitignore/api/node ### Node ### diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 0e59a459..00000000 --- a/Dockerfile +++ /dev/null @@ -1,61 +0,0 @@ -# Stage 1: Build stage -FROM node:alpine AS builder - -LABEL maintainer="https://github.com/its4nik" -LABEL version="2.0.1" -LABEL description="API for DockStat" -LABEL license="BSD-3-Clause license" -LABEL repository="https://github.com/its4nik/dockstatapi" -LABEL documentation="https://github.com/its4nik/dockstatapi" -LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" -LABEL org.opencontainers.image.licenses="BSD-3-Clause license" -LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" - -WORKDIR /build -ENV NODE_NO_WARNINGS=1 - -RUN apk add --update --no-cache bash - -COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ -RUN npm install - -COPY ./src ./src -RUN mv ./src/sample-variable.json ./src/data/variables.json -RUN npm run build:mini - -# Stage 2: main stage -FROM alpine AS main - -RUN apk add --update npm - -WORKDIR /build - -RUN mkdir -p /build/src/data - -COPY package*.json ./ -RUN npm install --omit=dev - -COPY --from=builder /build/dist/* /build/src -COPY --from=builder /build/src/misc/entrypoint.sh /build/entrypoint.sh -COPY --from=builder /build/src/misc/createEnvFile.sh /build/createEnvFile.sh - -RUN node src/config/db.js - -# Stage 3: Production stage -FROM alpine AS production - -WORKDIR /api - -RUN apk add --update --no-cache bash curl nodejs && \ - adduser -h /api -s /bin/bash -D dockstatapi dockstatapi && \ - chown -hR dockstatapi:dockstatapi /api - -USER dockstatapi - -HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:9876/api/status || exit 1 - -COPY --chown=dockstatapi:dockstatapi --from=main /build /api - -EXPOSE 9876 -ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/Dockerfile-dev b/Dockerfile-dev deleted file mode 100644 index ba9c01cb..00000000 --- a/Dockerfile-dev +++ /dev/null @@ -1,61 +0,0 @@ -# Stage 1: Build stage -FROM node:alpine AS builder - -LABEL maintainer="https://github.com/its4nik" -LABEL version="2.0.1" -LABEL description="API for DockStat" -LABEL license="BSD-3-Clause license" -LABEL repository="https://github.com/its4nik/dockstatapi" -LABEL documentation="https://github.com/its4nik/dockstatapi" -LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" -LABEL org.opencontainers.image.licenses="BSD-3-Clause license" -LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" - -WORKDIR /build -ENV NODE_NO_WARNINGS=1 - -RUN apk add --update --no-cache bash - -COPY tsconfig.json environment.d.ts package*.json tsconfig.json ./ -RUN npm install - -COPY ./src ./src -RUN mv ./src/sample-variable.json ./src/data/variables.json -RUN npm run build - -# Stage 2: main stage -FROM alpine AS main - -RUN apk add --update npm - -WORKDIR /build - -RUN mkdir -p /build/src/data - -COPY package*.json ./ -RUN npm install --omit=dev - -COPY --from=builder /build/dist/* /build/src -COPY --from=builder /build/src/misc/entrypoint.sh /build/entrypoint.sh -COPY --from=builder /build/src/misc/createEnvFile.sh /build/createEnvFile.sh - -RUN node src/config/db.js - -# Stage 3: Production stage -FROM alpine AS production - -WORKDIR /api - -RUN apk add --update --no-cache bash curl nodejs && \ - adduser -h /api -s /bin/bash -D dockstatapi dockstatapi && \ - chown -hR dockstatapi:dockstatapi /api - -USER dockstatapi - -HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:9876/api/status || exit 1 - -COPY --chown=dockstatapi:dockstatapi --from=main /build /api - -EXPOSE 9876 -ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/TODO.md b/TODO.md index fc40ce6f..7ac3d438 100644 --- a/TODO.md +++ b/TODO.md @@ -12,5 +12,6 @@ - [x] Update notification service - [x] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) - [ ] Better project structure -- [ ] Update logging => Better errors -- [ ] Update json responses and swagger +- [x] Update logging => Better errors +- [x] Update json responses +- [ ] Swagger update diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base new file mode 100644 index 00000000..1f9bf30d --- /dev/null +++ b/docker/Dockerfile-base @@ -0,0 +1,59 @@ +# Stage 1: Build stage +FROM node:alpine AS builder + +LABEL maintainer="https://github.com/its4nik" +LABEL version="2.0.1" +LABEL description="API for DockStat" +LABEL license="BSD-3-Clause license" +LABEL repository="https://github.com/its4nik/dockstatapi" +LABEL documentation="https://github.com/its4nik/dockstatapi" +LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" +LABEL org.opencontainers.image.licenses="BSD-3-Clause license" +LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" + +WORKDIR /app + +ENV NODE_NO_WARNINGS=1 + +RUN apk add --no-cache bash + +COPY tsconfig.json environment.d.ts package*.json ./ + +RUN export npm_config_cache=$(mktemp -d) && \ + npm install --production=false && \ + rm -rf $npm_config_cache /tmp/*.log + +COPY ./src ./src +RUN mv ./src/sample-variable.json ./src/data/variables.json +RUN npm run build:mini + +# Stage 2: Production stage +FROM node:alpine AS production + +WORKDIR /api + +RUN apk add --no-cache bash curl && \ + adduser -h /api -s /bin/bash -D dockstatapi + +HEALTHCHECK --interval=5m --timeout=3s \ + CMD curl -f http://localhost:9876/api/status || exit 1 + +COPY --chown=dockstatapi:dockstatapi --from=builder /app/dist/src /api/src +COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ + +RUN export npm_config_cache=$(mktemp -d) && \ + npm install --omit=dev && \ + rm -rf $npm_config_cache /tmp/*.log + +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh +RUN chmod +x /api/*.sh + +EXPOSE 9876 + +RUN chmod -R 777 /api/src/data /api && \ + chown -R dockstatapi:dockstatapi /api + +STOPSIGNAL 130 +USER dockstatapi +ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev new file mode 100644 index 00000000..58b9f43d --- /dev/null +++ b/docker/Dockerfile-dev @@ -0,0 +1,59 @@ +# Stage 1: Build stage +FROM node:alpine AS builder + +LABEL maintainer="https://github.com/its4nik" +LABEL version="2.0.1" +LABEL description="API for DockStat" +LABEL license="BSD-3-Clause license" +LABEL repository="https://github.com/its4nik/dockstatapi" +LABEL documentation="https://github.com/its4nik/dockstatapi" +LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" +LABEL org.opencontainers.image.licenses="BSD-3-Clause license" +LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" + +WORKDIR /app + +ENV NODE_NO_WARNINGS=1 + +RUN apk add --no-cache bash + +COPY tsconfig.json environment.d.ts package*.json ./ + +RUN export npm_config_cache=$(mktemp -d) && \ + npm install --production=false && \ + rm -rf $npm_config_cache /tmp/*.log + +COPY ./src ./src +RUN mv ./src/sample-variable.json ./src/data/variables.json +RUN npm run build + +# Stage 2: Production stage +FROM node:alpine AS production + +WORKDIR /api + +RUN apk add --no-cache bash curl && \ + adduser -h /api -s /bin/bash -D dockstatapi + +HEALTHCHECK --interval=5m --timeout=3s \ + CMD curl -f http://localhost:9876/api/status || exit 1 + +COPY --chown=dockstatapi:dockstatapi --from=builder /app/dist/src /api/src +COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ + +RUN export npm_config_cache=$(mktemp -d) && \ + npm install --omit=dev && \ + rm -rf $npm_config_cache /tmp/*.log + +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh +RUN chmod +x /api/*.sh + +EXPOSE 9876 + +RUN chmod -R 777 /api/src/data /api && \ + chown -R dockstatapi:dockstatapi /api + +STOPSIGNAL 130 +USER dockstatapi +ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/docker-compose.yaml b/docker/docker-compose.yaml similarity index 87% rename from docker-compose.yaml rename to docker/docker-compose.yaml index 4789b71c..225c5de2 100644 --- a/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -5,14 +5,16 @@ networks: services: master: container_name: master + user: "${UID:-1000}:${GID:-1000}" environment: - NODE_ENV=development - - HA_MASTER=false + - HA_MASTER=true - HA_MASTER_IP=master:9876 - HA_NODE=slave:9876 - HA_UNSAFE=true volumes: - - ./docker/master:/api/src/data + - ./master/data:/api/src/data + - ./master/logs:/api/logs ports: - 9876:9876 image: dockstatapi:local @@ -24,10 +26,12 @@ services: slave: container_name: slave + user: "${UID:-1000}:${GID:-1000}" environment: - NODE_ENV=development volumes: - - ./docker/slave:/api/src/data + - ./slave/data:/api/src/data + - ./slave/logs:/api/logs ports: - 6789:9876 image: dockstatapi:local diff --git a/package-lock.json b/package-lock.json index 1310ab8b..8c1ea14f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -872,26 +872,6 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -956,6 +936,19 @@ "node": ">=10" } }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@playwright/test": { "version": "1.49.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", @@ -1109,9 +1102,9 @@ "license": "MIT" }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", - "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.3.tgz", + "integrity": "sha512-JEhMNwUJt7bw728CydvYzntD0XJeTmDnvwLlbfbAhE7Tbslm/ax6bdIiUwTgeVlZTsJQPwZwKpAkyDtIjsvx3g==", "dev": true, "license": "MIT", "dependencies": { @@ -1142,9 +1135,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.3.tgz", + "integrity": "sha512-DifAyw4BkrufCILvD3ucnuN8eydUfc/C1GlyrnI+LK6543w5/L3VeVgf05o3B4fqSXP1dKYLOZsKfutpxPzZrw==", "dev": true, "license": "MIT", "dependencies": { @@ -1209,9 +1202,9 @@ } }, "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.68", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.68.tgz", - "integrity": "sha512-QGtpFH1vB99ZmTa63K4/FU8twThj4fuVSBkGddTp7uIL/cuoLWIUSL2RcOaigBhfR+hg5pgGkBnkoOxrTVBMKw==", + "version": "18.19.69", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.69.tgz", + "integrity": "sha512-ECPdY1nlaiO/Y6GUnwgtAAhLNaQ53AyIVz+eILxpEo5OvuqE6yWkqWBIb5dU0DqhKQtMeny+FBD3PK6lm7L5xQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1257,17 +1250,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.2.tgz", - "integrity": "sha512-adig4SzPLjeQ0Tm+jvsozSGiCliI2ajeURDGHjZ2llnA+A67HihCQ+a3amtPhUakd1GlwHxSRvzOZktbEvhPPg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.0.tgz", + "integrity": "sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/type-utils": "8.18.2", - "@typescript-eslint/utils": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", + "@typescript-eslint/scope-manager": "8.19.0", + "@typescript-eslint/type-utils": "8.19.0", + "@typescript-eslint/utils": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1287,16 +1280,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.2.tgz", - "integrity": "sha512-y7tcq4StgxQD4mDr9+Jb26dZ+HTZ/SkfqpXSiqeUXZHxOUyjWDKsmwKhJ0/tApR08DgOhrFAoAhyB80/p3ViuA==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.0.tgz", + "integrity": "sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/typescript-estree": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", + "@typescript-eslint/scope-manager": "8.19.0", + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/typescript-estree": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0", "debug": "^4.3.4" }, "engines": { @@ -1312,14 +1305,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.2.tgz", - "integrity": "sha512-YJFSfbd0CJjy14r/EvWapYgV4R5CHzptssoag2M7y3Ra7XNta6GPAJPPP5KGB9j14viYXyrzRO5GkX7CRfo8/g==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", + "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2" + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1330,14 +1323,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.2.tgz", - "integrity": "sha512-AB/Wr1Lz31bzHfGm/jgbFR0VB0SML/hd2P1yxzKDM48YmP7vbyJNHRExUE/wZsQj2wUCvbWH8poNHFuxLqCTnA==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.0.tgz", + "integrity": "sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.18.2", - "@typescript-eslint/utils": "8.18.2", + "@typescript-eslint/typescript-estree": "8.19.0", + "@typescript-eslint/utils": "8.19.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1354,9 +1347,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.2.tgz", - "integrity": "sha512-Z/zblEPp8cIvmEn6+tPDIHUbRu/0z5lqZ+NvolL5SvXWT5rQy7+Nch83M0++XzO0XrWRFWECgOAyE8bsJTl1GQ==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", + "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", "dev": true, "license": "MIT", "engines": { @@ -1368,14 +1361,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.2.tgz", - "integrity": "sha512-WXAVt595HjpmlfH4crSdM/1bcsqh+1weFRWIa9XMTx/XHZ9TCKMcr725tLYqWOgzKdeDrqVHxFotrvWcEsk2Tg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz", + "integrity": "sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/visitor-keys": "8.18.2", + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1395,16 +1388,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.2.tgz", - "integrity": "sha512-Cr4A0H7DtVIPkauj4sTSXVl+VBWewE9/o40KcF3TV9aqDEOWoXF3/+oRXNby3DYzZeCATvbdksYsGZzplwnK/Q==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.0.tgz", + "integrity": "sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.18.2", - "@typescript-eslint/types": "8.18.2", - "@typescript-eslint/typescript-estree": "8.18.2" + "@typescript-eslint/scope-manager": "8.19.0", + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/typescript-estree": "8.19.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1419,13 +1412,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.2.tgz", - "integrity": "sha512-zORcwn4C3trOWiCqFQP1x6G3xTRyZ1LYydnj51cRnJ6hxBlr/cKPckk+PKPUw/fXmvfKTcw7bwY3w9izgx5jZw==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.0.tgz", + "integrity": "sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.2", + "@typescript-eslint/types": "8.19.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1537,9 +1530,9 @@ } }, "node_modules/agentkeepalive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", - "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", "license": "MIT", "optional": true, "dependencies": { @@ -1919,6 +1912,19 @@ "node": ">= 10" } }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", @@ -3238,21 +3244,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", - "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", - "dunder-proto": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", + "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "math-intrinsics": "^1.0.0" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3261,6 +3267,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.0.tgz", + "integrity": "sha512-TtLgOcKaF1nMP2ijJnITkE4nRhbpshHhmzKiuhmSniiwWzovoqwqQ8rNuhf0mXJOqIY5iU+QkUe0CkJYrLsG9w==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-tsconfig": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", @@ -4014,19 +4033,6 @@ "node": ">=4" } }, - "node_modules/license-checker/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/license-checker/node_modules/nopt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", @@ -4505,15 +4511,16 @@ } }, "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, "bin": { "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" } }, "node_modules/mkdirp-classic": { @@ -4584,6 +4591,26 @@ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -6533,6 +6560,18 @@ "node": ">=8" } }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/teamcity-service-messages": { "version": "0.1.14", "resolved": "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz", @@ -6786,15 +6825,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.2.tgz", - "integrity": "sha512-KuXezG6jHkvC3MvizeXgupZzaG5wjhU3yE8E7e6viOvAvD9xAWYp8/vy0WULTGe9DYDWcQu7aW03YIV3mSitrQ==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.19.0.tgz", + "integrity": "sha512-Ni8sUkVWYK4KAcTtPjQ/UTiRk6jcsuDhPpxULapUDi8A/l8TSBk+t1GtJA1RsCzIJg0q6+J7bf35AwQigENWRQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.18.2", - "@typescript-eslint/parser": "8.18.2", - "@typescript-eslint/utils": "8.18.2" + "@typescript-eslint/eslint-plugin": "8.19.0", + "@typescript-eslint/parser": "8.19.0", + "@typescript-eslint/utils": "8.19.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 29e307e6..6b38fd68 100644 --- a/package.json +++ b/package.json @@ -6,18 +6,16 @@ "scripts": { "local-env-file": "bash ./src/misc/createEnvDev.sh", "start": "npm run local-env-file && tsx src/server.ts", - "start:build": "npx tsc && node dist/server.js", "dev": "npm run local-env-file && nodemon", "dev:trace": "npm run local-env-file && nodemon --trace-uncaught --trace-warnings", "dep": "bash ./src/misc/dependencyGraphs/createDependencyGraph.sh", "dep:remove": "bash ./src/misc/removeUnusedDeps.sh && npm run dep", "build": "npx tsc", "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", + "build:docker": "docker build . -t \"dockstatapi:local\" -f ./docker/Dockerfile-dev", "mini": "bash ./src/misc/minifyDist.sh", - "docker": "docker compose up -d", - "docker:full": "docker compose up -d && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", - "docker:build": "docker build . -t \"dockstatapi:local\" -f ./Dockerfile-dev && docker compose up -d", - "docker:build:full": "npm run docker:build && [ -z \"$TMUX\" ] && tmux new-session -d -s docker 'docker compose up -d && docker compose logs -f master' \\; split-window -v 'docker compose logs -f slave' \\; attach-session || echo 'Already inside a tmux session. Exiting.'; docker compose down", + "docker": "docker compose -f docker/docker-compose.yaml up -d && bash ./src/misc/.tmux.sh; docker compose -f docker/docker-compose.yaml down", + "docker:build": "npm run build:docker && npm run docker", "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write && npx prettier -c ./.github/workflows/*.yaml --parser yaml --write && npx prettier -c ./**/*.md --parser markdown --write && npx prettier -c ./**/*.json --parser json --write", "lint": "npx eslint", "lint:fix": "npx eslint --fix", diff --git a/src/config/hostsystem.ts b/src/config/hostsystem.ts index e9c04de3..0af379f6 100644 --- a/src/config/hostsystem.ts +++ b/src/config/hostsystem.ts @@ -1,4 +1,10 @@ -import { RUNNING_IN_DOCKER, VERSION } from "./variables"; +import { + RUNNING_IN_DOCKER, + VERSION, + HA_MASTER, + HA_UNSAFE, + TRUSTED_PROXYS, +} from "./variables"; import fs from "fs"; import logger from "../utils/logger"; import os from "os"; @@ -7,6 +13,8 @@ import { atomicWrite } from "../utils/atomicWrite"; const userConf = "./src/data/user.conf"; const inDocker: boolean = RUNNING_IN_DOCKER == "true"; const version: string = VERSION || "unknown"; +const masterNode: string = HA_MASTER === "true" ? "✓" : "✗"; +const unsafeSync: string = HA_UNSAFE === "true" ? "✓" : "✗"; function writeUserConf() { let previousConfig = null; @@ -54,9 +62,17 @@ function writeUserConf() { backendVersion: version, }; - logger.info( - `Starting at: ${startDetails.startedAt} - Version: ${startDetails.backendVersion} - Docker: ${installationDetails.inDocker} - Installed as: ${installationDetails.installedBy} - Platform: ${installationDetails.platform} - Arch: ${installationDetails.arch}`, - ); + logger.info("-----------------------------------------"); + logger.info(`Starting at : ${startDetails.startedAt}`); + logger.info(`Version : ${startDetails.backendVersion}`); + logger.info(`Docker : ${installationDetails.inDocker}`); + logger.info(`Running as : ${installationDetails.installedBy}`); + logger.info(`Platform : ${installationDetails.platform}`); + logger.info(`Arch : ${installationDetails.arch}`); + logger.info(`Master node : ${masterNode}`); + logger.info(`Unsafe sync : ${unsafeSync}`); + logger.info(`Proxies : ${TRUSTED_PROXYS}`); + logger.info("-----------------------------------------"); } export default writeUserConf; diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index 1b28b4f7..3e61b16f 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -172,7 +172,7 @@ async function synchronizeFilesWithNodes(): Promise { } const nodeUrl = - useUnsafeConnection == true + useUnsafeConnection === true ? `http://${node}/ha/sync` : `https://${node}/ha/sync`; @@ -259,7 +259,7 @@ async function ensureFileExists( const dirPath = path.dirname(filePath); await fs.promises.mkdir(dirPath, { recursive: true }); await fs.promises.writeFile(filePath, content, { flag: "w" }); - logger.info(`File created/updated: ${filePath}`); + logger.info(`File updated: ${filePath}`); } catch (error) { logger.error( `Error creating/updating file ${filePath}: ${(error as Error).message}`, diff --git a/src/misc/.tmux.sh b/src/misc/.tmux.sh new file mode 100644 index 00000000..a929a1a3 --- /dev/null +++ b/src/misc/.tmux.sh @@ -0,0 +1 @@ +[ -z "$TMUX" ] && tmux new-session -d -s docker 'docker compose -f docker/docker-compose.yaml logs -f master' \; rename-window 'master' \; new-window 'docker compose -f docker/docker-compose.yaml logs -f slave' \; rename-window 'slave' \; new-window 'docker compose -f docker/docker-compose.yaml logs -f test-socket-proxy' \; rename-window 'proxy' \; attach-session || echo 'Already inside a tmux session. Exiting.' diff --git a/src/server.ts b/src/server.ts index 6b680291..97e5337a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,4 @@ import express from "express"; -import logger from "./utils/logger"; import initializeApp from "./init"; import { startMasterNode } from "./controllers/highAvailability"; import writeUserConf from "./config/hostsystem"; @@ -7,10 +6,6 @@ import writeUserConf from "./config/hostsystem"; const app = express(); const PORT: number = 9876; -logger.info("Server starting up..."); -logger.info(`Server is running on http://localhost:${PORT}`); -logger.info(`Swagger docs available at http://localhost:${PORT}/api-docs\n`); - writeUserConf(); initializeApp(app); From 5df68298e495951f4e5b2d5bc6242227ad26c8df Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 2 Jan 2025 22:01:10 +0100 Subject: [PATCH 100/369] Fix: Run uglifyjs only on .js files --- src/misc/minifyDist.sh | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/misc/minifyDist.sh b/src/misc/minifyDist.sh index 0c256170..8a85b162 100755 --- a/src/misc/minifyDist.sh +++ b/src/misc/minifyDist.sh @@ -3,28 +3,28 @@ dist="$(pwd)/dist" run_script() { - npx uglifyjs --no-annotations --in-situ "$1" > /dev/null - echo "✔️ Minified : $(basename "$1")" + npx uglifyjs --no-annotations --in-situ "$1" > /dev/null + echo "✔️ Minified : $(basename "$1")" } if [ -d "$dist" ]; then - echo "::: Dist directory exists." + echo "::: Dist directory exists." else - echo "::: Dist does not exist... Running npx tsc" - npx tsc + echo "::: Dist does not exist... Running npx tsc" + npx tsc fi max_jobs=$(nproc) job_count=0 -for file in $(find "$dist" -type f); do - run_script "$file" & - ((job_count++)) +for file in $(find "$dist" -type f -name "*.js"); do + run_script "$file" & + ((job_count++)) - if ((job_count >= max_jobs)); then - wait - job_count=0 - fi + if ((job_count >= max_jobs)); then + wait + job_count=0 + fi done wait From 7fa7eeb7bce75625585bbf81e7fd39f818b8d295 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 2 Jan 2025 22:19:23 +0100 Subject: [PATCH 101/369] Err: Workflow wont run --- .github/workflows/validation.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 2226171e..10adc684 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -168,7 +168,7 @@ jobs: contents: read runs-on: ubuntu-24.04 if: github.ref_name == 'dev' - needs: [validation, test-building, Anchore, CodeQL] + needs: [test-building] steps: - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -193,12 +193,12 @@ jobs: flavor: | latest=false - - name: Build and push + - name: Build and Push Docker Images uses: docker/build-push-action@v6 with: - platforms: linux/amd64,linux/arm64, context: . file: docker/Dockerfile-dev + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} From 9175d9a99219abd241b40b35aecaf8560e87d031 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 07:32:43 +0100 Subject: [PATCH 102/369] Fix: Update validation.yaml --- .github/workflows/validation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 10adc684..ab37d212 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -197,7 +197,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - file: docker/Dockerfile-dev + file: docker/Dockerfile-base platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.metadata.outputs.tags }} From f39f1bc50437fa10a64643b295118fab14058cc2 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 07:48:56 +0100 Subject: [PATCH 103/369] Fix: Update validation.yaml --- .github/workflows/validation.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index ab37d212..a6e496de 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -170,6 +170,9 @@ jobs: if: github.ref_name == 'dev' needs: [test-building] steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Set up QEMU uses: docker/setup-qemu-action@v3 From 9bc2f651f54aa1b71b50ccb46ff1ff51863f8a9a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 08:11:13 +0100 Subject: [PATCH 104/369] Chore: Cleanup frontendConfiguration.json --- src/data/frontendConfiguration.json | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/data/frontendConfiguration.json b/src/data/frontendConfiguration.json index 884e0e20..fe51488c 100644 --- a/src/data/frontendConfiguration.json +++ b/src/data/frontendConfiguration.json @@ -1,16 +1 @@ -[ - { - "name": "test", - "tags": [ - "123", - "123", - "321" - ], - "link": "https://google.com", - "icon": "custom/test.png" - }, - { - "name": "test2", - "pinned": true - } -] \ No newline at end of file +[] From bb8a627aedbdf684d286d0375c66e150e4c27638 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 08:12:51 +0100 Subject: [PATCH 105/369] Fix: missing return in api.ts --- src/handlers/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/api.ts b/src/handlers/api.ts index 37529688..6f62c056 100644 --- a/src/handlers/api.ts +++ b/src/handlers/api.ts @@ -126,7 +126,7 @@ class ApiHandler { try { const rawData = fs.readFileSync(configPath); const data = JSON.parse(rawData.toString()); - ResponseHandler.rawData(data, "Fetched frontend configuration"); + return ResponseHandler.rawData(data, "Fetched frontend configuration"); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); return ResponseHandler.critical(errorMsg); From 987b550f4c2b3c2961836476b2d5d974d76d1f8f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 09:15:15 +0100 Subject: [PATCH 106/369] Chore: Change to dev Dockerfile for workflow --- .github/workflows/validation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index a6e496de..782a762c 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -200,7 +200,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - file: docker/Dockerfile-base + file: docker/Dockerfile-dev platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.metadata.outputs.tags }} From 621d1426840fc525a0b3ed9df58a8a5a6092595b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 09:23:39 +0100 Subject: [PATCH 107/369] Feat: Added docker scout --- .github/workflows/validation.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 782a762c..d15f1a4a 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -159,6 +159,23 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + - name: Analyze for critical and high CVEs + id: docker-scout-cves + if: ${{ github.event_name != 'pull_request_target' }} + uses: docker/scout-action@v1 + with: + command: cves + image: ${{ steps.meta.outputs.tags }} + sarif-file: sarif.output.json + summary: true + + - name: Upload SARIF result + id: upload-sarif + if: ${{ github.event_name != 'pull_request_target' }} + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: sarif.output.json + build-dev: name: "Dev-build" permissions: From d932c98ca15a17f665ef0637618fac110a6d0bca Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 09:25:55 +0100 Subject: [PATCH 108/369] Feat: Update workflow logic --- .github/workflows/validation.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index d15f1a4a..d6836785 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -114,7 +114,7 @@ jobs: sarif_file: ./results.sarif test-building: - needs: [validation, Anchore, CodeQL] + needs: [validation] runs-on: ubuntu-24.04 name: "Test building" permissions: @@ -185,7 +185,7 @@ jobs: contents: read runs-on: ubuntu-24.04 if: github.ref_name == 'dev' - needs: [test-building] + needs: [test-building, Anchore, CodeQL] steps: - name: Checkout Repository uses: actions/checkout@v3 From c87ba873a7e121643694e2d693e90d3a8987daac Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 09:31:23 +0100 Subject: [PATCH 109/369] Fix: Permissions --- .github/workflows/validation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index d6836785..6016534a 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -121,7 +121,7 @@ jobs: security-events: write packages: read actions: read - contents: read + contents: write steps: - name: Checkout repository uses: actions/checkout@v4 From 9cb349059663cd4a210cbe13d1adadfa236ea656 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 09:36:34 +0100 Subject: [PATCH 110/369] Fix: Workflow adjustment => drop docker scout :/ --- .github/workflows/validation.yaml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 6016534a..7414ee52 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -159,23 +159,6 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - - name: Analyze for critical and high CVEs - id: docker-scout-cves - if: ${{ github.event_name != 'pull_request_target' }} - uses: docker/scout-action@v1 - with: - command: cves - image: ${{ steps.meta.outputs.tags }} - sarif-file: sarif.output.json - summary: true - - - name: Upload SARIF result - id: upload-sarif - if: ${{ github.event_name != 'pull_request_target' }} - uses: github/codeql-action/upload-sarif@v2 - with: - sarif_file: sarif.output.json - build-dev: name: "Dev-build" permissions: From 7b09298399a903590210acb1b1abb4b5168b6ae2 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 09:47:03 +0100 Subject: [PATCH 111/369] Fix: Remove unnecessary matrix --- .github/workflows/validation.yaml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 7414ee52..7392c838 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -49,13 +49,6 @@ jobs: actions: read contents: read - strategy: - fail-fast: false - matrix: - include: - - language: javascript-typescript - build-mode: none - steps: - name: Checkout repository uses: actions/checkout@v4 @@ -64,20 +57,9 @@ jobs: uses: github/codeql-action/init@v3 with: languages: javascript-typescript - build-mode: ${{ matrix.build-mode }} + build-mode: none queries: security-extended - - name: Check build mode - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: From 204d4bf037bcd8225c8cbf22117d8faf59bdfcc0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 14:27:59 +0100 Subject: [PATCH 112/369] Chore: Pre release workflow --- .github/workflows/validation.yaml | 55 ++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 7392c838..1763ee96 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -1,6 +1,10 @@ name: "Run all tests" -on: [push] +on: + push: + release: + types: + - published jobs: validation: @@ -189,3 +193,52 @@ jobs: labels: ${{ steps.metadata.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + + build-pre-release: + name: "Pre-Release-build" + permissions: + security-events: read + packages: write + actions: read + contents: read + runs-on: ubuntu-24.04 + if: "!github.event.release.prerelease" + needs: [test-building, Anchore, CodeQL] + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ github.token }} + + - name: Generate Docker tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,enable=true,priority=200,prefix=,suffix=,value=pre + flavor: | + latest=false + + - name: Build and Push Docker Images + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile-dev + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From b27369fdc2697b04ff9b847d1da957e08f3e825a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 3 Jan 2025 14:34:57 +0100 Subject: [PATCH 113/369] Fix: removed negation from if in workflow --- .github/workflows/validation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 1763ee96..7040e940 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -202,7 +202,7 @@ jobs: actions: read contents: read runs-on: ubuntu-24.04 - if: "!github.event.release.prerelease" + if: "github.event.release.prerelease" needs: [test-building, Anchore, CodeQL] steps: - name: Checkout Repository From fe37f2daa0ef3068348788af4aa874824399d693 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 5 Jan 2025 14:16:25 +0100 Subject: [PATCH 114/369] Feat: Added new image => See next commit --- .github/DockStat-dark.png | Bin 0 -> 82847 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/DockStat-dark.png diff --git a/.github/DockStat-dark.png b/.github/DockStat-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..00ac779a6dcb303ce5c690aa079dedc175e5c8f3 GIT binary patch literal 82847 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

TLgl9u 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 AX#fBK literal 0 HcmV?d00001 From a82d021d68ee30fe949dc35e894f20932223fe8e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 29 Jan 2025 20:19:01 +0100 Subject: [PATCH 115/369] Fix: Add checkout --- .github/workflows/build-image.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 41f18cb8..5e0db4ce 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -12,6 +12,9 @@ jobs: build-release: runs-on: ubuntu-24.04 steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up QEMU uses: docker/setup-qemu-action@v3 From 0f3ed69ac6d319fdbcbcb3d841cf4b637dcbc57b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 9 Feb 2025 14:37:41 +0100 Subject: [PATCH 116/369] Feat: Add WebSockets, graph generation service and HA (#31) * Chore: new testing workflows (dropping playwright) * Chore: updating github workflow * Fix: Fixing some minor things * Chore: Updated to ES2020 syntax and AMD module * Feat: startServer function to start the server with a different port * Fix: Adjusting testing files based on workflow restrictions * Fix: Adjusting testing files based on workflow restrictions * Chore: Updating swagger (wrong branch bruh) * Docs: Update swagger documentation (#26) * Chore: Updated swagger * Fix: Typo * Fix: Fixing dockerfiles for prod/dev environment * Feat: Add `/graph` and `/graph/image` endpoints (#27) * Feat: Server side HTML generation => Client side rendering * Fix: This _might_ fix the workflow * Fix: Remove unused function * Fix: Please make it stop * Fix: Setting up python before hand * Fix: Remove unused dep * Fix: Using node20 instead of latest * Fix: Works on my end... * Feat: Master Nodes * Feat: Icon for master node (needs testing) * Fix: Adjusting function (needs testing) * Fix: Adjusting function (needs testing) * Fix: Removed some graph rendering features (will be back but better) * Feat: render html file as png using puppeteer ToFix: svgs dont render * Fix: Hell yeah we got image creation! * Fix: Adjusted routes since they need an absolute path * Fix: Remove unused dependencies * Fix: Exclude CWE-200 from CodeQl * Feat: Respomse examples in swagger Fix: Fixing some catch blocks * Fix: Adjusting catches * Fix: Adjusted catch to typing * Feat: Stack creation * Feat: Stack creation + starting and stopping * Fix: Project root instead of path Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Fix: Logging adustment * Fix: Allow undescores and dashes in stack name Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Fix: Propagate error * Fix: Inline variable that is immediately returned Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Fix: move some things around * Fix: Minor adjustments * Feat: Get a stack's docker-compose * Feat: automatic Stack environmental file management * Fix: sample-varaible.json adjustment * Fix: Potential fix for code scanning alert no. 102: Log injection Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Fix: fix for code scanning alert no. 94: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Fix: fix for code scanning alert no. 92: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * FiX: fix for code scanning alert no. 106: Log injection Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Fix: Logger vulnerability and CI graph generation * Feat: change logger verbosity and spelling fix * Feat: ToDo comments to GH issue * Fix: Add checkout * Fix: May fix the ToDo workflow * Fix: Remove todo * Fix: Re-Add commit * Fix: Remove TODO * Fix: Re-add TODO * Fix: Where tf did my package lock go :sob: * CI/CD: Remove ToDo * CI/CD: Add ToDo * CI/CD: Fix command * CI/CD: Add checkout * Fix: CPU value was a percentage the whole time? * Feat: Websocket endpoints for logs and container metrics * Fix: Make linter happy * Fix: Fix import * Fix: Fix tsc build * Jest: Fix tests * Jest: Fix Tests * Fix: Typo in src/config/swagger.yaml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Fix: Typo in src/config/swagger.yaml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Fix: Tyypo in src/config/swagger.yaml Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * (code-quality): Inline variable that is immediately returned Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * (code-quality): Prefer object destructuring when accessing and using properties. Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * (code-quality): Prefer object destructuring when accessing and using properties. Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * (code-quality): Prefer object destructuring when accessing and using properties. Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Fix: Update extractHostData.ts * Update TODO.md --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: ItsNik --- .dockerignore | 7 +- .github/workflows/build-image.yaml | 24 +- .github/workflows/validation.yaml | 67 +- .gitignore | 3 + CREDITS.md | 106 +- README.md | 1 + TODO.md | 5 +- __tests__/auth.spec.ts | 38 + __tests__/config.spec.ts | 49 + __tests__/database.spec.ts | 35 + __tests__/frontend.spec.ts | 125 + __tests__/getters.spec.ts | 99 + __tests__/util/previousResponse.ts | 23 + docker/Dockerfile-base | 19 +- docker/Dockerfile-dev | 19 +- docker/docker-compose.dev.yaml | 40 + docker/docker-compose.yaml | 1 + environment.d.ts | 38 +- eslint.config.mjs | 2 +- nodemon.json | 3 +- package-lock.json | 9462 ++++++++++++++--- package.json | 74 +- playwright.config.ts | 37 - src/config/db.ts | 2 +- src/config/hostsystem.ts | 21 +- src/config/initFiles.ts | 1 + src/config/stacks.ts | 260 + src/config/swagger.yaml | 2095 ++++ src/config/swaggerConfig.ts | 59 +- src/config/swaggerTheme.ts | 6 + src/config/variables.ts | 4 +- src/controllers/containerController.ts | 2 +- src/controllers/fetchData.ts | 7 +- src/controllers/frontendConfiguration.ts | 46 +- src/controllers/highAvailability.ts | 38 +- src/controllers/proxy.ts | 4 +- src/controllers/scheduler.ts | 8 +- src/data/frontendConfiguration.json | 2 +- src/handlers/api.ts | 8 +- src/handlers/conf.ts | 9 +- src/handlers/data.ts | 30 + src/handlers/graph.ts | 257 + src/handlers/notification.ts | 5 +- src/handlers/response.ts | 2 +- src/handlers/stack.ts | 162 + src/init.ts | 36 +- src/middleware/authMiddleware.ts | 2 +- src/misc/createEnvDev.sh | 14 +- src/misc/createEnvFile.sh | 14 +- .../dependencyGraphs/createDependencyGraph.sh | 2 +- src/misc/dependencyGraphs/mermaid-all.txt | 166 +- src/misc/dependencyGraphs/mermaid-graph.txt | 15 + src/misc/entrypoint.sh | 8 +- src/misc/minifyDist.sh | 2 +- src/routes/auth/routes.ts | 42 - src/routes/data/routes.ts | 134 - src/routes/frontendController/routes.ts | 441 - src/routes/getter/routes.ts | 273 - src/routes/graphs/routes.ts | 31 + src/routes/highavailability/routes.ts | 30 - src/routes/notifications/routes.ts | 109 - src/routes/setter/routes.ts | 73 +- src/routes/stack/routes.ts | 35 + src/sample-variable.json | 6 +- src/server.ts | 18 +- src/typings/dockerCompose.ts | 92 + src/typings/dockerStackEnv.ts | 10 + src/typings/ha.ts | 2 +- src/typings/stackConfig.ts | 5 + src/utils/assets/api-icon.svg | 1 + src/utils/assets/container-icon.svg | 1 + src/utils/assets/server-icon.svg | 1 + src/utils/atomicWrite.ts | 2 +- src/utils/connectionChecker.ts | 4 +- src/utils/containerService.ts | 223 +- src/utils/dockerClient.ts | 14 +- src/utils/extractHostData.ts | 73 +- src/utils/logger.ts | 10 +- src/utils/notifications/_template.ts | 9 +- src/utils/notifications/email.ts | 3 +- src/utils/startServer.ts | 18 + src/utils/swaggerDocs.ts | 9 +- src/utils/webSocket.ts | 113 + tests/main.spec.ts | 131 - tsconfig.json | 2 +- 85 files changed, 12086 insertions(+), 3393 deletions(-) create mode 100644 __tests__/auth.spec.ts create mode 100644 __tests__/config.spec.ts create mode 100644 __tests__/database.spec.ts create mode 100644 __tests__/frontend.spec.ts create mode 100644 __tests__/getters.spec.ts create mode 100644 __tests__/util/previousResponse.ts create mode 100644 docker/docker-compose.dev.yaml delete mode 100644 playwright.config.ts create mode 100644 src/config/stacks.ts create mode 100644 src/config/swagger.yaml create mode 100644 src/config/swaggerTheme.ts create mode 100644 src/handlers/graph.ts create mode 100644 src/handlers/stack.ts create mode 100644 src/misc/dependencyGraphs/mermaid-graph.txt create mode 100644 src/routes/graphs/routes.ts create mode 100644 src/routes/stack/routes.ts create mode 100644 src/typings/dockerCompose.ts create mode 100644 src/typings/dockerStackEnv.ts create mode 100644 src/typings/stackConfig.ts create mode 100644 src/utils/assets/api-icon.svg create mode 100644 src/utils/assets/container-icon.svg create mode 100644 src/utils/assets/server-icon.svg create mode 100644 src/utils/startServer.ts create mode 100644 src/utils/webSocket.ts delete mode 100644 tests/main.spec.ts diff --git a/.dockerignore b/.dockerignore index 2d993096..6381947a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,11 +1,12 @@ # custom paths: src/data/* -*.md -*.txt -docker +.tmp +docker/master +docker/slave .test* # Created by https://www.toptal.com/developers/gitignore/api/node ### Node ### +*-audit.json # Logs logs *.log diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 5e0db4ce..bbb4875d 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -1,4 +1,4 @@ -name: "Build dockstatapi:latest" +name: "Build and Push Docker Image" on: release: @@ -21,22 +21,34 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Github Container Registry + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ github.token }} - - name: Generate Docker tags + - name: Extract version and create tag + id: get-tag + run: | + # Remove 'v' prefix from release tag if present + VERSION="${GITHUB_REF#refs/tags/v}" + # Check if pre-release and append '-pre' + if ${{ github.event.release.prerelease }}; then + TAG="$VERSION-pre" + else + TAG="$VERSION" + fi + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - name: Generate Docker metadata uses: docker/metadata-action@v5 id: metadata with: images: ghcr.io/${{ github.repository }} tags: | - type=sha,format=long,prefix= - flavor: | - latest=true + type=raw,value=${{ steps.get-tag.outputs.tag }} + type=raw,value=latest,enable=${{ !github.event.release.prerelease }} - name: Build and push uses: docker/build-push-action@v6 diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 7040e940..7e2b685c 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -26,7 +26,7 @@ jobs: cache: npm - name: Install dependencies - run: npm ci --ignore-scripts + run: npm ci - name: Create varaibles.json run: npm run local-env-file @@ -43,8 +43,43 @@ jobs: - name: Audit packages run: npm audit --audit-level=high - CodeQL: + - name: Jests + run: npm run test:silent + + ToDo: needs: validation + runs-on: ubuntu-20.04 + name: "ToDo comment to issue" + permissions: + contents: write + issues: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: "TODO to Issue" + uses: "alstr/todo-to-issue-action@v5" + with: + INSERT_ISSUE_URLS: "true" + + - name: Set Git user + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Commit and Push Changes + run: | + git add -A + if [[ `git status --porcelain` ]]; then + git commit -m "Automatically added GitHub issue links to TODOs" + git push + else + echo "No changes to commit" + fi + + CodeQL: + needs: [ToDo] runs-on: ubuntu-24.04 name: "Analyze TypeScript" permissions: @@ -57,12 +92,21 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: javascript-typescript build-mode: none queries: security-extended + config: | + query-filter: + - exclude: + tags: /cwe-200/ - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 @@ -70,7 +114,7 @@ jobs: category: "/language:javascript-typescript" Anchore: - needs: validation + needs: [ToDo] runs-on: ubuntu-24.04 name: "Anchore" permissions: @@ -82,6 +126,11 @@ jobs: - name: Set up Grype installation path run: echo "$HOME/bin" >> $GITHUB_PATH + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Download Grype run: | curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin @@ -100,7 +149,7 @@ jobs: sarif_file: ./results.sarif test-building: - needs: [validation] + needs: [ToDo] runs-on: ubuntu-24.04 name: "Test building" permissions: @@ -112,6 +161,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -159,6 +213,11 @@ jobs: - name: Checkout Repository uses: actions/checkout@v3 + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.gitignore b/.gitignore index dc93b889..9e264ac0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,12 @@ # custom paths: src/data/* +src/data/frontendConfiguration.json +.tmp docker/master docker/slave .test* +stacks # Created by https://www.toptal.com/developers/gitignore/api/node ### Node ### *-audit.json diff --git a/CREDITS.md b/CREDITS.md index 050b430b..50b66abb 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -8,35 +8,81 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) | ----------------- | -------------------------------------------- | -------------------- | | spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | +### License: Apache 2.0 + +| Name | Repository | Publisher | +| ---------------------- | ------------------------------------------ | --------- | +| qrcode-terminal@0.12.0 | https://github.com/gtanner/qrcode-terminal | N/A | + ### License: Apache-2.0 -| Name | Repository | Publisher | -| ------------------------------------ | ------------------------------------------------------------- | --------------------- | -| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | -| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @playwright/test@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | -| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | -| doctrine@3.0.0 | https://github.com/eslint/doctrine | N/A | -| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | -| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | -| playwright-core@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| playwright@1.49.1 | https://github.com/microsoft/playwright | Microsoft Corporation | -| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | -| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | -| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | +| Name | Repository | Publisher | +| ------------------------------------ | ------------------------------------------------------------------------ | -------------------- | +| @ampproject/remapping@2.3.0 | https://github.com/ampproject/remapping | Justin Ridgewell | +| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | +| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | +| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | +| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | +| @puppeteer/browsers@2.7.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/browsers | The Chromium Authors | +| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | +| @sigstore/bundle@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | +| @sigstore/core@2.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | +| @sigstore/protobuf-specs@0.3.2 | https://github.com/sigstore/protobuf-specs | bdehamer@github.com | +| @sigstore/sign@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | +| @sigstore/tuf@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | +| @sigstore/verify@2.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | +| b4a@1.6.7 | https://github.com/holepunchto/b4a | Holepunch | +| bare-events@2.5.4 | https://github.com/holepunchto/bare-events | Holepunch | +| bare-fs@2.3.5 | https://github.com/holepunchto/bare-fs | Holepunch | +| bare-os@2.4.4 | https://github.com/holepunchto/bare-os | Holepunch | +| bare-path@2.1.3 | https://github.com/holepunchto/bare-path | Holepunch | +| bare-stream@2.6.1 | https://github.com/holepunchto/bare-stream | Holepunch | +| bser@2.1.1 | https://github.com/facebook/watchman | Wez Furlong | +| chromium-bidi@0.11.0 | https://github.com/GoogleChromeLabs/chromium-bidi | The Chromium Authors | +| chromium-bidi@0.12.0 | https://github.com/GoogleChromeLabs/chromium-bidi | The Chromium Authors | +| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | +| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| ejs@3.1.10 | https://github.com/mde/ejs | Matthew Eernisse | +| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | +| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | +| exponential-backoff@3.1.1 | https://github.com/coveo/exponential-backoff | Sami Sayegh | +| fb-watchman@2.0.2 | https://github.com/facebook/watchman | Wez Furlong | +| filelist@1.0.4 | https://github.com/mde/filelist | Matthew Eernisse | +| human-signals@2.1.0 | https://github.com/ehmicky/human-signals | ehmicky | +| jake@10.9.2 | https://github.com/jakejs/jake | Matthew Eernisse | +| puppeteer-core@24.0.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core | The Chromium Authors | +| puppeteer@24.0.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer | The Chromium Authors | +| sigstore@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | +| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | +| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| text-decoder@1.2.3 | https://github.com/holepunchto/text-decoder | Holepunch | +| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | +| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | +| walker@1.0.8 | https://github.com/daaku/nodejs-walker | Naitik Shah | + +### License: Artistic-2.0 + +| Name | Repository | Publisher | +| ---------- | -------------------------- | ----------- | +| npm@11.0.0 | https://github.com/npm/cli | GitHub Inc. | + +### License: BlueOak-1.0.0 + +| Name | Repository | Publisher | +| ---------------------------- | ------------------------------------------------ | ------------------ | +| chownr@3.0.0 | https://github.com/isaacs/chownr | Isaac Z. Schlueter | +| jackspeak@3.4.3 | https://github.com/isaacs/jackspeak | Isaac Z. Schlueter | +| package-json-from-dist@1.0.1 | https://github.com/isaacs/package-json-from-dist | Isaac Z. Schlueter | +| path-scurry@1.11.1 | https://github.com/isaacs/path-scurry | Isaac Z. Schlueter | +| yallist@5.0.0 | https://github.com/isaacs/yallist | Isaac Z. Schlueter | ### License: CC-BY-3.0 @@ -44,6 +90,12 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) | --------------------- | -------------------------------------------------- | -------------------- | | spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | +### License: CC-BY-4.0 + +| Name | Repository | Publisher | +| ------------------------- | -------------------------------------------- | ---------- | +| caniuse-lite@1.0.30001690 | https://github.com/browserslist/caniuse-lite | Ben Briggs | + ### License: Python-2.0 | Name | Repository | Publisher | diff --git a/README.md b/README.md index 8fc800a8..25778667 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ With this new release a couple of extra features (compared to v1) are going to b - Advanced security through middlewares: rate-limiting and authentication - Multi Arch Docker builds through docker buildx - High Availability using single master and unlimited worker nodes! +- Dynamically created Graphs # 🔗 DockStatAPI v2 Documentation diff --git a/TODO.md b/TODO.md index 7ac3d438..44a128d3 100644 --- a/TODO.md +++ b/TODO.md @@ -7,11 +7,12 @@ - [x] Structure code differently - [x] Write new README and make the docs better - [x] Update more files to correct TS syntax => remove "any" -- [ ] Websockets +- [X] Websockets - [x] Better /api/status endpoint with connection status of each host - [x] Update notification service - [x] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) - [ ] Better project structure - [x] Update logging => Better errors - [x] Update json responses -- [ ] Swagger update +- [X] Swagger update +- [ ] Edge case testing diff --git a/__tests__/auth.spec.ts b/__tests__/auth.spec.ts new file mode 100644 index 00000000..bcf0eb21 --- /dev/null +++ b/__tests__/auth.spec.ts @@ -0,0 +1,38 @@ +export const testPass = "123456789"; +import { Server } from 'http'; +import supertest from "supertest"; +import { startServer } from "../src/utils/startServer"; +import app from "../src/server"; + +const port = 13001; +const server = new Server(app); + +startServer(app, server, port); + +const request = supertest(`http://localhost:${port}`); + +describe("Authentication", () => { + it("Enable Authentication", async () => { + const res = await request.post(`/auth/enable?password=${testPass}`); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + expect(res.body).toHaveProperty( + "message", + "Authentication enabled successfully", + ); + }); + + it("Test no password", async () => { + const res = await request.get("/api/status"); + expect(res.status).toEqual(403); + expect(res.type).toEqual(expect.stringContaining("json")); + }); + + it("Disable authentication", async () => { + const res = await request + .post(`/auth/disable?password=${testPass}`) + .set("x-password", testPass); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + }); +}); \ No newline at end of file diff --git a/__tests__/config.spec.ts b/__tests__/config.spec.ts new file mode 100644 index 00000000..d6356004 --- /dev/null +++ b/__tests__/config.spec.ts @@ -0,0 +1,49 @@ +import supertest from "supertest"; +import { startServer } from "../src/utils/startServer"; +import app from "../src/server"; +import { Server } from 'http'; + +const port = 13002; +const server = new Server(app); + +startServer(app, server, port); + +const request = supertest(`http://localhost:${port}`); + +const mockServerName: string = "mockstatapi"; +const mockServerIP: string = "127.0.0.1"; +const mockServerPort: number = 2375; + +describe("Config endpoints", () => { + it("Add an host", async () => { + let res = await request.put( + `/conf/addHost?name=${mockServerName}&url=${mockServerIP}&port=${mockServerPort}`, + ); + expect(res.status).toEqual(200); + + res = await request.get("/api/hosts"); + expect(res.status).toEqual(200); + expect(res.body).toContain("mockstatapi"); + }); + + it("Adjust scheduler", async () => { + let res = await request.put("/conf/scheduler?interval=10m"); + expect(res.status).toEqual(200); + + res = await request.get("/api/current-schedule"); + expect(res.status).toEqual(200); + + // Reset to standart 5m + res = await request.put("/conf/scheduler?interval=5m"); + expect(res.status).toEqual(200); + }); + + it("Remove Host from config", async () => { + let res = await request.delete(`/conf/removeHost?hostName=mockstatapi`); + expect(res.status).toEqual(200); + + res = await request.get("/api/hosts"); + expect(res.status).toEqual(200); + expect(res.body).not.toHaveProperty("mockstatapi"); + }); +}); diff --git a/__tests__/database.spec.ts b/__tests__/database.spec.ts new file mode 100644 index 00000000..c0c46c1b --- /dev/null +++ b/__tests__/database.spec.ts @@ -0,0 +1,35 @@ +import supertest from "supertest"; +import { startServer } from "../src/utils/startServer"; +import app from "../src/server"; +import { Server } from 'http'; + +const port = 13003; +const server = new Server(app); + +startServer(app, server, port); + +const request = supertest(`http://localhost:${port}`); + +describe("Database", () => { + it("Get latest database entry", async () => { + const res = await request.get("/data/latest"); + expect(res.status).toEqual(200); + }); + + it("Get all database entries", async () => { + const res = await request.get("/data/all"); + expect(res.status).toEqual(200); + }); + + it("Clear database", async () => { + let res = await request.delete("/data/clear"); + expect(res.status).toEqual(200); + + res = await request.get("/data/latest"); + expect(res.status).toEqual(404); + expect(res.body).toHaveProperty( + "message", + "No data available for /data/latest", + ); + }); +}); diff --git a/__tests__/frontend.spec.ts b/__tests__/frontend.spec.ts new file mode 100644 index 00000000..753b98da --- /dev/null +++ b/__tests__/frontend.spec.ts @@ -0,0 +1,125 @@ +import supertest from "supertest"; +import { startServer } from "../src/utils/startServer"; +import app from "../src/server"; +import { Server } from 'http'; + +const port = 13004; +const server = new Server(app); + +startServer(app, server, port); + +const request = supertest(`http://localhost:${port}`); + +const sec: number = 1000; + +const mockContainer: string = "dockstatapi"; +const mockLink: string = "https://github.com/its4nik/dockstatapi"; +const mockIcon: string = "dockstatapi.png"; +const mockTag1: string = "backend"; +const mockTag2: string = "local"; + +const verifiedResponse = [ + { + name: "dockstatapi", + tags: ["backend", "local"], + pinned: true, + link: "https://github.com/its4nik/dockstatapi", + icon: "dockstatapi.png", + hidden: true, + }, +]; + + + +describe("Test frontend specific configurations", () => { + it( + "Setup the configuration file", + async () => { + // Hide container + let res = await request.delete(`/frontend/hide/${mockContainer}`); + + expect(res.status).toEqual(200); + + // Add Tag(s) + res = await request.post(`/frontend/tag/${mockContainer}/${mockTag1}`); + + expect(res.status).toEqual(200); + res = await request.post(`/frontend/tag/${mockContainer}/${mockTag2}`); + + expect(res.status).toEqual(200); + + // Pin container + res = await request.post(`/frontend/pin/${mockContainer}`); + + expect(res.status).toEqual(200); + + // Add link + res = await request.post( + `/frontend/add-link/${mockContainer}/${encodeURIComponent(mockLink)}`, + ); + + expect(res.status).toEqual(200); + + // Add icon + res = await request.post( + `/frontend/add-icon/${mockContainer}/${mockIcon}/false`, + ); + + expect(res.status).toEqual(200); + }, + 60 * sec, + ); + + it("Verify the configuration", async () => { + const res = await request.get("/api/frontend-config"); + + expect(res.status).toEqual(200); + expect(res.body).toEqual(verifiedResponse); + }); + + it( + "Reset configuration", + async () => { + // Show container + let res = await request.post(`/frontend/show/${mockContainer}`); + + expect(res.status).toEqual(200); + + // Remove tag(s) + res = await request.delete( + `/frontend/remove-tag/${mockContainer}/${mockTag1}`, + ); + + expect(res.status).toEqual(200); + + res = await request.delete( + `/frontend/remove-tag/${mockContainer}/${mockTag2}`, + ); + + expect(res.status).toEqual(200); + + // Unpin + res = await request.delete(`/frontend/unpin/${mockContainer}`); + + expect(res.status).toEqual(200); + + // Remove link + res = await request.delete(`/frontend/remove-link/${mockContainer}`); + + expect(res.status).toEqual(200); + + // Remove icon + res = await request.delete(`/frontend/remove-icon/${mockContainer}`); + + expect(res.status).toEqual(200); + }, + 60 * sec, + ); + + it("Verify the reset configuration", async () => { + const res = await request.get("/api/frontend-config"); + + expect(res.status).toEqual(200); + expect(res.body).toEqual([]); + }); +}); diff --git a/__tests__/getters.spec.ts b/__tests__/getters.spec.ts new file mode 100644 index 00000000..3ba5950b --- /dev/null +++ b/__tests__/getters.spec.ts @@ -0,0 +1,99 @@ +import { createPreviousResponse } from "./util/previousResponse"; +import supertest from "supertest"; +import { startServer } from "../src/utils/startServer"; +import app from "../src/server"; +import { Server } from 'http'; + +const port = 13005; +const server = new Server(app); + +startServer(app, server, port); + +const request = supertest(`http://localhost:${port}`); +const PreviousResponse = createPreviousResponse(); + +describe("Get endpoints", () => { + it("GET /api/hosts", async () => { + const res = await request.get("/api/hosts"); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + + const hosts: string[] = res.body; + + if (hosts.length >= 1) { + expect(Array.isArray(hosts)).toBe(true); + expect(hosts.length).toBeGreaterThan(0); + expect(typeof hosts[0]).toBe("string"); + PreviousResponse.set(hosts[0]); + } + }); + + it("GET /api/host/:host/stats", async () => { + const host = PreviousResponse.get(); + + if (!host) { + console.log("No hosts found, skipping /api/host/:host/stats test"); + return; + } + + const res = await request.get(`/api/host/${host}/stats`); + + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + }); + + it("GET /api/system", async () => { + const res = await request.get("/api/system"); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + }); + + it("GET /api/status", async () => { + const res = await request.get("/api/status"); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + expect(res.body).toHaveProperty("ApiReachable", true); + }); + + it("GET /api/containers", async () => { + const res = await request.get("/api/containers"); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + }); + + it("GET /api/config", async () => { + const res = await request.get("/api/config"); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + expect(res.body).toHaveProperty("hosts"); + }); + + it("GET /api/current-schedule", async () => { + const res = await request.get("/api/current-schedule"); + + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + expect(res.body).toHaveProperty("interval"); + }); + + it("GET /api/frontend-config", async () => { + const res = await request.get("/api/frontend-config"); + + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + }); + + it("GET /ha/config", async () => { + const res = await request.get("/ha/config"); + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + }); + + it("GET /notification-service/get-template", async () => { + const res = await request.get("/notification-service/get-template"); + + expect(res.status).toEqual(200); + expect(res.type).toEqual(expect.stringContaining("json")); + expect(res.body).toHaveProperty("text"); + }); +}); diff --git a/__tests__/util/previousResponse.ts b/__tests__/util/previousResponse.ts new file mode 100644 index 00000000..774a862a --- /dev/null +++ b/__tests__/util/previousResponse.ts @@ -0,0 +1,23 @@ +let response: string = ""; + +class PreviousResponse { + set(body: unknown): void { + try { + response = JSON.stringify(body).replace(/[" ]/g, ""); + } catch (error: unknown) { + console.error("Error in setting response:", error); + throw new Error("Failed to set response"); + } + } + + get(): string { + try { + return response; + } catch (error: unknown) { + console.error("Error in getting response:", error); + throw new Error("Failed to get response"); + } + } +} + +export const createPreviousResponse = () => new PreviousResponse(); diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 1f9bf30d..76cec4c9 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -1,5 +1,5 @@ # Stage 1: Build stage -FROM node:alpine AS builder +FROM node:20-alpine AS builder LABEL maintainer="https://github.com/its4nik" LABEL version="2.0.1" @@ -19,16 +19,14 @@ RUN apk add --no-cache bash COPY tsconfig.json environment.d.ts package*.json ./ -RUN export npm_config_cache=$(mktemp -d) && \ - npm install --production=false && \ - rm -rf $npm_config_cache /tmp/*.log +RUN npm install --production=false COPY ./src ./src RUN mv ./src/sample-variable.json ./src/data/variables.json RUN npm run build:mini # Stage 2: Production stage -FROM node:alpine AS production +FROM node:20-alpine AS production WORKDIR /api @@ -40,10 +38,10 @@ HEALTHCHECK --interval=5m --timeout=3s \ COPY --chown=dockstatapi:dockstatapi --from=builder /app/dist/src /api/src COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/config/swagger.yaml /api/src/config/swagger.yaml +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/utils/assets /api/src/utils/assets -RUN export npm_config_cache=$(mktemp -d) && \ - npm install --omit=dev && \ - rm -rf $npm_config_cache /tmp/*.log +RUN npm install --omit=dev COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh @@ -51,9 +49,10 @@ RUN chmod +x /api/*.sh EXPOSE 9876 -RUN chmod -R 777 /api/src/data /api && \ +RUN mkdir -p /api/src/data && \ + chmod -R 777 /api/src/data /api && \ chown -R dockstatapi:dockstatapi /api STOPSIGNAL 130 USER dockstatapi -ENTRYPOINT [ "bash", "./entrypoint.sh" ] +ENTRYPOINT [ "bash", "./entrypoint.sh", "--prod" ] diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index 58b9f43d..43a42402 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -1,5 +1,5 @@ # Stage 1: Build stage -FROM node:alpine AS builder +FROM node:20-alpine AS builder LABEL maintainer="https://github.com/its4nik" LABEL version="2.0.1" @@ -19,16 +19,14 @@ RUN apk add --no-cache bash COPY tsconfig.json environment.d.ts package*.json ./ -RUN export npm_config_cache=$(mktemp -d) && \ - npm install --production=false && \ - rm -rf $npm_config_cache /tmp/*.log +RUN npm install --production=false COPY ./src ./src RUN mv ./src/sample-variable.json ./src/data/variables.json RUN npm run build # Stage 2: Production stage -FROM node:alpine AS production +FROM node:20-alpine AS production WORKDIR /api @@ -40,10 +38,10 @@ HEALTHCHECK --interval=5m --timeout=3s \ COPY --chown=dockstatapi:dockstatapi --from=builder /app/dist/src /api/src COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/config/swagger.yaml /api/src/config/swagger.yaml +COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/utils/assets /api/src/utils/assets -RUN export npm_config_cache=$(mktemp -d) && \ - npm install --omit=dev && \ - rm -rf $npm_config_cache /tmp/*.log +RUN npm install COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh @@ -51,9 +49,10 @@ RUN chmod +x /api/*.sh EXPOSE 9876 -RUN chmod -R 777 /api/src/data /api && \ +RUN mkdir -p /api/src/data && \ + chmod -R 777 /api/src/data /api && \ chown -R dockstatapi:dockstatapi /api STOPSIGNAL 130 USER dockstatapi -ENTRYPOINT [ "bash", "./entrypoint.sh" ] +ENTRYPOINT [ "bash", "./entrypoint.sh", "--dev" ] diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml new file mode 100644 index 00000000..7bc3773f --- /dev/null +++ b/docker/docker-compose.dev.yaml @@ -0,0 +1,40 @@ +services: + test-socket-proxy: + image: lscr.io/linuxserver/socket-proxy:latest + container_name: test-socket-proxy + environment: + - ALLOW_START=1 #optional + - ALLOW_STOP=1 #optional + - ALLOW_RESTARTS=1 #optional + - AUTH=0 #optional + - BUILD=0 #optional + - COMMIT=0 #optional + - CONFIGS=0 #optional + - CONTAINERS=1 #optional + - DISABLE_IPV6=0 #optional + - DISTRIBUTION=0 #optional + - EVENTS=1 #optional + - EXEC=0 #optional + - IMAGES=0 #optional + - INFO=1 #optional + - NETWORKS=1 #optional + - NODES=1 #optional + - PING=1 #optional + - POST=0 #optional + - PLUGINS=0 #optional + - SECRETS=0 #optional + - SERVICES=0 #optional + - SESSION=0 #optional + - SWARM=0 #optional + - SYSTEM=0 #optional + - TASKS=0 #optional + - VERSION=1 #optional + - VOLUMES=0 #optional + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + restart: unless-stopped + read_only: true + tmpfs: + - /run + ports: + - 2375:2375 \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 225c5de2..436d8a21 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -79,3 +79,4 @@ services: - /run networks: - shared-network + diff --git a/environment.d.ts b/environment.d.ts index 803ae43c..df2595f5 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -2,41 +2,9 @@ declare global { namespace NodeJS { interface ProcessEnv { // Node specific: - NODE_ENV: "development" | "production"; - TRUSTED_PROXYS: string | undefined; - - // User.conf - RUNNING_IN_DOCKER: string | undefined; - VERSION: string | undefined; - - // High Availability - HA_MASTER: string | undefined; //bool - HA_MASTER_IP: string | undefined; - HA_NODE: string | undefined; //ip list with port seperated by "," like: "10.0.0.4:5012,10.0.0.5:9876" - HA_UNSAFE: string | undefined; - - // Notification services: - DISCORD_WEBHOOK_URL: string | undefined; - - EMAIL_SENDER: string | undefined; - EMAIL_RECIPIENT: string | undefined; - EMAIL_PASSWORD: string | undefined; - EMAIL_SERVICE: string | undefined; - - PUSHBULLET_ACCESS_TOKEN: string | undefined; - - PUSHOVER_USER_KEY: string | undefined; - PUSHOVER_API_TOKEN: string | undefined; - - SLACK_WEBHOOK_URL: string | undefined; - - TELEGRAM_BOT_TOKEN: string | undefined; - TELEGRAM_CHAT_ID: string | undefined; - - WHATSAPP_API_URL: string | undefined; - WHATSAPP_RECIPIENT: string | undefined; - - CUSTOM_NOTIFICATION: string | undefined; // enter the script name without .js here and without custom/... + NODE_ENV: "development" | "production" | "testing"; + PORT: string | undefined; + CI: "true" | null; } } } diff --git a/eslint.config.mjs b/eslint.config.mjs index 5b7b70a1..56994a62 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,7 @@ import tseslint from "typescript-eslint"; /** @type {import('eslint').Linter.Config[]} */ export default [ { ignores: ["node_modules/*", "dist/*"] }, - { files: ["src/*.{ts}"] }, + { files: ["src/**/*.ts"] }, { languageOptions: { globals: globals.node } }, pluginJs.configs.recommended, ...tseslint.configs.recommended, diff --git a/nodemon.json b/nodemon.json index 9d946e97..be32c75d 100644 --- a/nodemon.json +++ b/nodemon.json @@ -4,7 +4,8 @@ "src/logs", "**/fixtures/**", ".gitignore", - "**/*.json" + "**/*.json", + "**/__tests__/**" ], "execMap": { "ts": "tsx" diff --git a/package-lock.json b/package-lock.json index 8c1ea14f..6efc7ed3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,40 +12,52 @@ "bcrypt": "^5.1.1", "chokidar": "^4.0.1", "cors": "^2.8.5", + "cytoscape": "^3.30.4", + "docker-compose": "^1.1.0", "dockerode": "^4.0.2", "express": "^4.21.1", "express-rate-limit": "^7.4.1", "https": "^1.0.0", + "i": "^0.3.7", "ipaddr.js": "^2.2.0", "nodemailer": "^6.9.16", + "npm": "^11.0.0", + "puppeteer": "^24.0.0", "sqlite3": "^5.1.7", - "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", "winston": "^3.15.0", - "winston-daily-rotate-file": "^5.0.0" + "winston-daily-rotate-file": "^5.0.0", + "yamljs": "^0.3.0" }, "devDependencies": { "@eslint/js": "^9.17.0", - "@playwright/test": "^1.49.0", "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", + "@types/cytoscape": "^3.21.8", "@types/dockerode": "^3.3.31", "@types/express": "^5.0.0", "@types/express-handlebars": "^5.3.1", + "@types/jest": "^29.5.14", "@types/node": "^22.9.0", + "@types/node-fetch": "^2.6.12", "@types/nodemailer": "^6.4.17", + "@types/supertest": "^6.0.2", "@types/supports-color": "^8.1.3", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.7", + "@types/ws": "^8.5.14", + "@types/yamljs": "^0.2.34", "@typescript-eslint/eslint-plugin": "^8.18.2", "@typescript-eslint/parser": "^8.18.2", "dependency-cruiser": "^16.5.0", "eslint": "^9.17.0", "globals": "^15.14.0", + "jest": "^29.7.0", "license-checker": "^25.0.1", "nodemon": "^3.1.7", - "ora": "^8.1.1", "prettier": "^3.4.2", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "tsx": "^4.19.2", "typescript-eslint": "^8.18.2", @@ -55,314 +67,588 @@ "npm": ">=10.8.2" } }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", - "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", - "license": "MIT", + "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": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", - "js-yaml": "^4.1.0" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@apidevtools/openapi-schemas": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", - "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, "engines": { - "node": ">=10" + "node": ">=6.9.0" } }, - "node_modules/@apidevtools/swagger-methods": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", - "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", - "license": "MIT" + "node_modules/@babel/compat-data": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@apidevtools/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "node_modules/@babel/core": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz", + "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", + "dev": true, "license": "MIT", "dependencies": { - "@apidevtools/json-schema-ref-parser": "^9.0.6", - "@apidevtools/openapi-schemas": "^2.0.4", - "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "call-me-maybe": "^1.0.1", - "z-schema": "^5.0.1" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.7", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.26.7", + "@babel/types": "^7.26.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, - "peerDependencies": { - "openapi-types": ">=7" + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@balena/dockerignore": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", - "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", - "license": "Apache-2.0" + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, "engines": { - "node": ">=0.1.90" + "node": ">=6.9.0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, "license": "MIT", "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "cpu": [ - "arm" - ], + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "cpu": [ - "x64" - ], + "node_modules/@babel/helpers": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", + "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/parser": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@babel/types": "^7.26.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "cpu": [ - "arm" - ], + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "cpu": [ - "ia32" - ], + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "cpu": [ - "loong64" - ], + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "cpu": [ - "mips64el" - ], + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-ppc64": { + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", + "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "license": "Apache-2.0" + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", "cpu": [ "ppc64" ], @@ -370,50 +656,50 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "aix" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-riscv64": { + "node_modules/@esbuild/android-arm": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", "cpu": [ - "riscv64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-s390x": { + "node_modules/@esbuild/android-arm64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-x64": { + "node_modules/@esbuild/android-x64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", "cpu": [ "x64" ], @@ -421,67 +707,67 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-arm64": { + "node_modules/@esbuild/darwin-x64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { + "node_modules/@esbuild/freebsd-arm64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "freebsd" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { + "node_modules/@esbuild/freebsd-x64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", "cpu": [ "x64" ], @@ -489,32 +775,253 @@ "license": "MIT", "optional": true, "os": [ - "sunos" + "freebsd" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-arm64": { + "node_modules/@esbuild/linux-arm": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-ia32": { + "node_modules/@esbuild/linux-arm64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", "cpu": [ "ia32" @@ -576,13 +1083,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -615,9 +1122,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", - "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", + "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -713,9 +1220,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", - "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", + "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", "dev": true, "license": "MIT", "engines": { @@ -723,9 +1230,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -733,18 +1240,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", - "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.10.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -752,6 +1273,37 @@ "license": "MIT", "optional": true }, + "node_modules/@grpc/grpc-js": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.6.tgz", + "integrity": "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -818,14 +1370,455 @@ "url": "https://github.com/sponsors/nzakas" } }, - "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==", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { @@ -836,21 +1829,25 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", @@ -949,40 +1946,167 @@ "node": ">=10" } }, - "node_modules/@playwright/test": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", - "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.49.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" }, - "node_modules/@tootallnate/once": { + "node_modules/@protobufjs/base64": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6" - } + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" }, - "node_modules/@tsconfig/node10": { + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@puppeteer/browsers": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.7.1.tgz", + "integrity": "sha512-MK7rtm8JjaxPN7Mf1JdZIZKPD2Z+W7osvrC1vjpvfOX1K0awDIHYbNi89f7eotp7eMUn2shWnt03HwVbriXtKQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.0", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.0", + "tar-fs": "^3.0.8", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-fs": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", + "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", @@ -1010,6 +2134,51 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, "node_modules/@types/bcrypt": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", @@ -1041,6 +2210,13 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", @@ -1051,6 +2227,13 @@ "@types/node": "*" } }, + "node_modules/@types/cytoscape": { + "version": "3.21.9", + "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.9.tgz", + "integrity": "sha512-JyrG4tllI6jvuISPjHK9j2Xv/LTbnLekLke5otGStjFluIyA9JjgnvgZrSBsp8cEDpiTjwgZUZwpPv8TSBcoLw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/docker-modem": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", @@ -1063,9 +2246,9 @@ } }, "node_modules/@types/dockerode": { - "version": "3.3.32", - "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.32.tgz", - "integrity": "sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg==", + "version": "3.3.34", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.34.tgz", + "integrity": "sha512-mH9SuIb8NuTDsMus5epcbTzSbEo52fKLBMo0zapzYIAIyfDqoIFn7L3trekHLKC8qmxGV++pPUP4YqQ9n5v2Zg==", "dev": true, "license": "MIT", "dependencies": { @@ -1102,9 +2285,9 @@ "license": "MIT" }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.3.tgz", - "integrity": "sha512-JEhMNwUJt7bw728CydvYzntD0XJeTmDnvwLlbfbAhE7Tbslm/ax6bdIiUwTgeVlZTsJQPwZwKpAkyDtIjsvx3g==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1114,6 +2297,16 @@ "@types/send": "*" } }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -1121,10 +2314,56 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/mime": { @@ -1135,15 +2374,25 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.3.tgz", - "integrity": "sha512-DifAyw4BkrufCILvD3ucnuN8eydUfc/C1GlyrnI+LK6543w5/L3VeVgf05o3B4fqSXP1dKYLOZsKfutpxPzZrw==", - "dev": true, + "version": "22.13.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz", + "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/nodemailer": { "version": "6.4.17", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", @@ -1155,9 +2404,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.17", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", - "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", "dev": true, "license": "MIT" }, @@ -1192,9 +2441,9 @@ } }, "node_modules/@types/ssh2": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz", - "integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.4.tgz", + "integrity": "sha512-9JTQgVBWSgq6mAen6PVnrAmty1lqgCMvpfN+1Ck5WRUsyMYPa6qd50/vMJ0y1zkGpOEgLzm8m8Dx/Y5vRouLaA==", "dev": true, "license": "MIT", "dependencies": { @@ -1202,9 +2451,9 @@ } }, "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.69", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.69.tgz", - "integrity": "sha512-ECPdY1nlaiO/Y6GUnwgtAAhLNaQ53AyIVz+eILxpEo5OvuqE6yWkqWBIb5dU0DqhKQtMeny+FBD3PK6lm7L5xQ==", + "version": "18.19.75", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.75.tgz", + "integrity": "sha512-UIksWtThob6ZVSyxcOqCLOUNg/dyO1Qvx4McgeuhrEtHTLFTf7BBhEazaE4K806FGTPtzd/2sE90qn4fVr7cyw==", "dev": true, "license": "MIT", "dependencies": { @@ -1218,6 +2467,37 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", + "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/supports-color": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", @@ -1249,22 +2529,66 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yamljs": { + "version": "0.2.34", + "resolved": "https://registry.npmjs.org/@types/yamljs/-/yamljs-0.2.34.tgz", + "integrity": "sha512-gJvfRlv9ErxdOv7ux7UsJVePtX54NAvQyd8ncoiFqK8G5aeHIfQfGH2fbruvjAQ9657HwAaO54waS+Dsk2QTUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.0.tgz", - "integrity": "sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz", + "integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/type-utils": "8.19.0", - "@typescript-eslint/utils": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/scope-manager": "8.23.0", + "@typescript-eslint/type-utils": "8.23.0", + "@typescript-eslint/utils": "8.23.0", + "@typescript-eslint/visitor-keys": "8.23.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1280,16 +2604,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.0.tgz", - "integrity": "sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", + "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/scope-manager": "8.23.0", + "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/typescript-estree": "8.23.0", + "@typescript-eslint/visitor-keys": "8.23.0", "debug": "^4.3.4" }, "engines": { @@ -1305,14 +2629,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", - "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", + "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0" + "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/visitor-keys": "8.23.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1323,16 +2647,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.0.tgz", - "integrity": "sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz", + "integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/utils": "8.19.0", + "@typescript-eslint/typescript-estree": "8.23.0", + "@typescript-eslint/utils": "8.23.0", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1347,9 +2671,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", - "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", + "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", "dev": true, "license": "MIT", "engines": { @@ -1361,20 +2685,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz", - "integrity": "sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", + "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/visitor-keys": "8.23.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1388,16 +2712,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.0.tgz", - "integrity": "sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz", + "integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/typescript-estree": "8.19.0" + "@typescript-eslint/scope-manager": "8.23.0", + "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/typescript-estree": "8.23.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1412,13 +2736,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.0.tgz", - "integrity": "sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", + "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/types": "8.23.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1573,6 +2897,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1586,7 +2926,6 @@ "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" @@ -1690,65 +3029,289 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, - "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==", + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" }, - "node_modules/bcrypt": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", - "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", - "hasInstallScript": true, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, "license": "MIT", "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "node-addon-api": "^5.0.0" + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" }, "engines": { - "node": ">= 10.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" } }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { - "tweetnacl": "^0.14.3" + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.1.tgz", + "integrity": "sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^3.0.0", + "bare-stream": "^2.0.0" + }, + "engines": { + "bare": ">=1.7.0" + } + }, + "node_modules/bare-os": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.4.0.tgz", + "integrity": "sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.6.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "license": "MIT", "engines": { @@ -1840,6 +3403,62 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1864,6 +3483,22 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -1912,6 +3547,19 @@ "node": ">= 10" } }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cacache/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -1925,6 +3573,13 @@ "node": ">=10" } }, + "node_modules/cacache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", @@ -1954,22 +3609,46 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", - "license": "MIT" - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001698", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001698.tgz", + "integrity": "sha512-xJ3km2oiG/MbNU8G6zIq6XRZ6HtAOVXsbOrP/blGazi52kc5Yy7b6sDA5O+FbROzRrV7BSTllLHuNvmawYUJjw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1987,6 +3666,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2011,6 +3700,42 @@ "node": ">=10" } }, + "node_modules/chromium-bidi": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-1.2.0.tgz", + "integrity": "sha512-XtdJ1GSN6S3l7tO7F77GhNsw0K367p0IsLYf2yZawCVAKKC3lUvDhPdMVrB2FNhmhfW43QGYbEX3Wg6q0maGwQ==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -2021,35 +3746,38 @@ "node": ">=6" } }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", "dependencies": { - "restore-cursor": "^5.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" } }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -2064,7 +3792,6 @@ "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": { "color-name": "~1.1.4" @@ -2123,16 +3850,39 @@ "text-hex": "1.0.x" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2166,6 +3916,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -2181,6 +3938,13 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2194,6 +3958,32 @@ "node": ">= 0.10" } }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cpu-features": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", @@ -2208,6 +3998,28 @@ "node": ">=10.0.0" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -2230,6 +4042,24 @@ "node": ">= 8" } }, + "node_modules/cytoscape": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.0.tgz", + "integrity": "sha512-zDGn1K/tfZwEnoGOcHc0H4XazqAAXAuDpcYw9mUnUjATjqljyCNGJv8uEvbvxGaGHaVshxMecyl6oc6uKzRfbw==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2273,6 +4103,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -2289,6 +4134,40 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -2305,9 +4184,9 @@ } }, "node_modules/dependency-cruiser": { - "version": "16.8.0", - "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.8.0.tgz", - "integrity": "sha512-VyBzIrLHfG7rT36URln+CTy8VSjrLB7YDlMx5vtBSHRHCOXgLUCcP4n5ZoD+s166T0i5LN33q1CvBkEOGsDTSg==", + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.9.0.tgz", + "integrity": "sha512-Gc/xHNOBq1nk5i7FPCuexCD0m2OXB/WEfiSHfNYQaQaHZiZltnl5Ixp/ZG38Jvi8aEhKBQTHV4Aw6gmR7rWlOw==", "dev": true, "license": "MIT", "dependencies": { @@ -2317,9 +4196,9 @@ "acorn-loose": "^8.4.0", "acorn-walk": "^8.3.4", "ajv": "^8.17.1", - "commander": "^12.1.0", - "enhanced-resolve": "^5.17.1", - "ignore": "^6.0.2", + "commander": "^13.0.0", + "enhanced-resolve": "^5.18.0", + "ignore": "^7.0.0", "interpret": "^3.1.1", "is-installed-globally": "^1.0.0", "json5": "^2.2.3", @@ -2347,9 +4226,9 @@ } }, "node_modules/dependency-cruiser/node_modules/ignore": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", - "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", + "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", "dev": true, "license": "MIT", "engines": { @@ -2375,6 +4254,22 @@ "node": ">=8" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1402036", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1402036.tgz", + "integrity": "sha512-JwAYQgEvm3yD45CHB+RmF5kMbWtXBaOGwuxa87sZogHcLCv8c/IqnThaoQ1y60d7pXWjSKWQphPEc+1rAScVdg==", + "license": "BSD-3-Clause" + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -2396,10 +4291,32 @@ "node": ">=0.3.1" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/docker-compose": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.1.0.tgz", + "integrity": "sha512-VrkQJNafPQ5d6bGULW0P6KqcxSkv3ZU5Wn2wQA19oB71o7+55vQ9ogFe2MMeNbK+jc9rrKVy280DnHO5JLMWOQ==", + "license": "MIT", + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/docker-modem": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", - "integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz", + "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==", "license": "Apache-2.0", "dependencies": { "debug": "^4.1.1", @@ -2412,31 +4329,23 @@ } }, "node_modules/dockerode": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz", - "integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.4.tgz", + "integrity": "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w==", "license": "Apache-2.0", "dependencies": { "@balena/dockerignore": "^1.0.2", - "docker-modem": "^5.0.3", - "tar-fs": "~2.0.1" + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.6", + "protobufjs": "^7.3.2", + "tar-fs": "~2.0.1", + "uuid": "^10.0.0" }, "engines": { "node": ">= 8.0" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2457,6 +4366,42 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.96", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.96.tgz", + "integrity": "sha512-8AJUW6dh75Fm/ny8+kZKJzI1pgoE8bKLZlzDU2W1ENd+DXKJrx7I7l9hb8UWR4ojlnb5OlixMt00QWiYJoVw1w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2511,9 +4456,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", - "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -2529,7 +4474,6 @@ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "license": "MIT", - "optional": true, "engines": { "node": ">=6" } @@ -2541,6 +4485,15 @@ "license": "MIT", "optional": true }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2560,9 +4513,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2611,6 +4564,15 @@ "@esbuild/win32-x64": "0.23.1" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2630,20 +4592,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { - "version": "9.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", - "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.0.tgz", + "integrity": "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", + "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.17.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/js": "9.20.0", + "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", @@ -2812,6 +4795,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -2842,7 +4838,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -2866,6 +4861,39 @@ "node": ">= 0.6" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -2875,6 +4903,23 @@ "node": ">=6" } }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -2951,6 +4996,41 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2958,10 +5038,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -2969,7 +5055,7 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -3002,28 +5088,64 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" }, "node_modules/file-entry-cache": { "version": "8.0.0", @@ -3053,6 +5175,29 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3143,6 +5288,36 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", + "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^2.0.0", + "once": "^1.4.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3186,9 +5361,9 @@ "license": "ISC" }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3230,17 +5405,23 @@ "node": ">=10" } }, - "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-intrinsic": { @@ -3267,10 +5448,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.0.tgz", - "integrity": "sha512-TtLgOcKaF1nMP2ijJnITkE4nRhbpshHhmzKiuhmSniiwWzovoqwqQ8rNuhf0mXJOqIY5iU+QkUe0CkJYrLsG9w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3280,10 +5471,23 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", - "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", "dev": true, "license": "MIT", "dependencies": { @@ -3293,6 +5497,20 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -3450,6 +5668,16 @@ "node": ">= 0.4" } }, + "node_modules/hexoid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", + "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -3457,6 +5685,13 @@ "dev": true, "license": "ISC" }, + "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-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -3481,18 +5716,25 @@ } }, "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "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", - "optional": true, "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" } }, "node_modules/https": { @@ -3514,6 +5756,16 @@ "node": ">= 6" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -3524,6 +5776,14 @@ "ms": "^2.0.0" } }, + "node_modules/i": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz", + "integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -3574,10 +5834,9 @@ "license": "ISC" }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -3590,6 +5849,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3659,7 +5938,6 @@ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "license": "MIT", - "optional": true, "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" @@ -3678,9 +5956,9 @@ } }, "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, "node_modules/is-binary-path": { @@ -3731,6 +6009,16 @@ "node": ">=8" } }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3761,19 +6049,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -3816,19 +6091,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3836,490 +6098,637 @@ "devOptional": true, "license": "ISC" }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", + "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-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "argparse": "^2.0.1" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=10" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "license": "MIT", - "optional": true - }, - "node_modules/json-buffer": { + "node_modules/istanbul-lib-report": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/istanbul-lib-report/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": { - "json-buffer": "3.0.1" + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=8" } }, - "node_modules/license-checker": { - "version": "25.0.1", - "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", - "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", "dev": true, - "license": "BSD-3-Clause", + "license": "Apache-2.0", "dependencies": { - "chalk": "^2.4.1", - "debug": "^3.1.0", - "mkdirp": "^0.5.1", - "nopt": "^4.0.1", - "read-installed": "~4.0.3", - "semver": "^5.5.0", - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0", - "spdx-satisfies": "^4.0.0", - "treeify": "^1.1.0" + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" }, "bin": { - "license-checker": "bin/license-checker" + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" } }, - "node_modules/license-checker/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=4" + "node": "*" } }, - "node_modules/license-checker/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">=4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/license-checker/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/license-checker/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/license-checker/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.1" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/license-checker/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, "engines": { - "node": ">=0.8.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/license-checker/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">=4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/license-checker/node_modules/nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "abbrev": "1", - "osenv": "^0.1.4" + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, - "bin": { - "nopt": "bin/nopt.js" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/license-checker/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/license-checker/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, - "license": "MIT" - }, - "node_modules/lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", - "license": "MIT" + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" }, "engines": { - "node": ">=18" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "fsevents": "^2.3.2" } }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, "license": "MIT", "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, "engines": { - "node": ">= 12.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "optional": true, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" }, "engines": { - "node": ">=10" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, "engines": { - "node": ">=8" + "node": ">=6" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, - "license": "ISC" - }, - "node_modules/make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "license": "ISC", - "optional": true, - "dependencies": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/memoize": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.0.0.tgz", - "integrity": "sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==", + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "license": "MIT", "dependencies": { - "mimic-function": "^5.0.0" + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/memoize?sponsor=1" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, "engines": { - "node": ">= 8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, "engines": { - "node": ">=8.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/micromatch/node_modules/picomatch": { + "node_modules/jest-util/node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", @@ -4332,57 +6741,30 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", "engines": { "node": ">=10" }, @@ -4390,507 +6772,3688 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", "dependencies": { - "yallist": "^4.0.0" + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "license": "ISC", - "optional": true, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", "dependencies": { - "minipass": "^3.0.0" + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, "engines": { - "node": ">= 8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, - "optionalDependencies": { - "encoding": "^0.1.12" + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "license": "ISC", - "optional": true, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", "dependencies": { - "minipass": "^3.0.0" + "argparse": "^2.0.1" }, - "engines": { - "node": ">=8" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" }, "engines": { - "node": ">=8" + "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/license-checker": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", + "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "chalk": "^2.4.1", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "read-installed": "~4.0.3", + "semver": "^5.5.0", + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-satisfies": "^4.0.0", + "treeify": "^1.1.0" + }, + "bin": { + "license-checker": "bin/license-checker" + } + }, + "node_modules/license-checker/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/license-checker/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/license-checker/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/license-checker/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/license-checker/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/license-checker/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/license-checker/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/long": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memoize": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.0.0.tgz", + "integrity": "sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/memoize?sponsor=1" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/minizlib": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "license": "MIT", + "optional": true + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-abi": { + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/node-gyp/node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", + "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", + "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/nodemon/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.1.0.tgz", + "integrity": "sha512-rPMBrZud26lI/LcjQeLw/K5Hf1apXMKgkpNNEzp0YQYmM877+T1ZNKPcB2hnTi7e6fBNz8xLtMMn/w46fVUqGw==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/redact", + "@npmcli/run-script", + "@sigstore/tuf", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "cli-columns", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "normalize-package-data", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "spdx-expression-parse", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which" + ], + "license": "Artistic-2.0", + "workspaces": [ + "docs", + "smoke-tests", + "mock-globals", + "mock-registry", + "workspaces/*" + ], + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^9.0.0", + "@npmcli/config": "^10.0.1", + "@npmcli/fs": "^4.0.0", + "@npmcli/map-workspaces": "^4.0.2", + "@npmcli/package-json": "^6.1.1", + "@npmcli/promise-spawn": "^8.0.2", + "@npmcli/redact": "^3.0.0", + "@npmcli/run-script": "^9.0.1", + "@sigstore/tuf": "^3.0.0", + "abbrev": "^3.0.0", + "archy": "~1.0.0", + "cacache": "^19.0.1", + "chalk": "^5.4.1", + "ci-info": "^4.1.0", + "cli-columns": "^4.0.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.3", + "glob": "^10.4.5", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^8.0.2", + "ini": "^5.0.0", + "init-package-json": "^8.0.0", + "is-cidr": "^5.1.0", + "json-parse-even-better-errors": "^4.0.0", + "libnpmaccess": "^10.0.0", + "libnpmdiff": "^8.0.0", + "libnpmexec": "^10.0.0", + "libnpmfund": "^7.0.0", + "libnpmorg": "^8.0.0", + "libnpmpack": "^9.0.0", + "libnpmpublish": "^11.0.0", + "libnpmsearch": "^9.0.0", + "libnpmteam": "^8.0.0", + "libnpmversion": "^8.0.0", + "make-fetch-happen": "^14.0.3", + "minimatch": "^9.0.5", + "minipass": "^7.1.1", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^11.0.0", + "nopt": "^8.0.0", + "normalize-package-data": "^7.0.0", + "npm-audit-report": "^6.0.0", + "npm-install-checks": "^7.1.1", + "npm-package-arg": "^12.0.1", + "npm-pick-manifest": "^10.0.0", + "npm-profile": "^11.0.1", + "npm-registry-fetch": "^18.0.2", + "npm-user-validate": "^3.0.0", + "p-map": "^7.0.3", + "pacote": "^21.0.0", + "parse-conflict-json": "^4.0.0", + "proc-log": "^5.0.0", + "qrcode-terminal": "^0.12.0", + "read": "^4.0.0", + "semver": "^7.6.3", + "spdx-expression-parse": "^4.0.0", + "ssri": "^12.0.0", + "supports-color": "^9.4.0", + "tar": "^6.2.1", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^6.0.0", + "which": "^5.0.0" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true, + "license": "ISC" + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "inBundle": 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/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "inBundle": 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/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/agent": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^4.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/map-workspaces": "^4.0.1", + "@npmcli/metavuln-calculator": "^9.0.0", + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.1", + "@npmcli/query": "^4.0.0", + "@npmcli/redact": "^3.0.0", + "@npmcli/run-script": "^9.0.1", + "bin-links": "^5.0.0", + "cacache": "^19.0.1", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^8.0.0", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^10.2.2", + "minimatch": "^9.0.4", + "nopt": "^8.0.0", + "npm-install-checks": "^7.1.0", + "npm-package-arg": "^12.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.1", + "pacote": "^21.0.0", + "parse-conflict-json": "^4.0.0", + "proc-log": "^5.0.0", + "proggy": "^3.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "read-package-json-fast": "^4.0.0", + "semver": "^7.3.7", + "ssri": "^12.0.0", + "treeverse": "^3.0.0", + "walk-up-path": "^4.0.0" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "10.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^4.0.1", + "@npmcli/package-json": "^6.0.1", + "ci-info": "^4.0.0", + "ini": "^5.0.0", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "6.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "4.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^19.0.0", + "json-parse-even-better-errors": "^4.0.0", + "pacote": "^21.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "6.1.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "8.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^6.1.2" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/redact": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "9.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/@sigstore/bundle": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/core": { + "version": "2.0.0", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.3.3", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/sign": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^14.0.1", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/verify": { + "version": "2.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "7.1.3", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "6.2.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^7.0.0", + "npm-normalize-package-bin": "^4.0.0", + "proc-log": "^5.0.0", + "read-cmd-shim": "^5.0.0", + "write-file-atomic": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "19.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/minizlib": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/mkdirp": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/tar": { + "version": "7.4.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "4.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "4.1.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^5.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "7.0.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.4.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/diff": { + "version": "7.0.0", + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/eastasianwidth": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.1", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/foreground-child": { + "version": "3.3.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "3.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "10.4.5", + "inBundle": 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/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "8.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.1.1", + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "7.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "7.0.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/ini": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^6.1.0", + "npm-package-arg": "^12.0.0", + "promzard": "^2.0.0", + "read": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/ip-address": { + "version": "9.0.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "5.1.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^4.1.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/jackspeak": { + "version": "3.4.3", + "inBundle": 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/npm/node_modules/jsbn": { + "version": "1.1.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "10.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^12.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "binary-extensions": "^3.0.0", + "diff": "^7.0.0", + "minimatch": "^9.0.4", + "npm-package-arg": "^12.0.0", + "pacote": "^21.0.0", + "tar": "^6.2.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "10.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.0.0", + "@npmcli/run-script": "^9.0.1", + "ci-info": "^4.0.0", + "npm-package-arg": "^12.0.0", + "pacote": "^21.0.0", + "proc-log": "^5.0.0", + "read": "^4.0.0", + "read-package-json-fast": "^4.0.0", + "semver": "^7.3.7", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.0.0", + "@npmcli/run-script": "^9.0.1", + "npm-package-arg": "^12.0.0", + "pacote": "^21.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "11.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "ci-info": "^4.0.0", + "normalize-package-data": "^7.0.0", + "npm-package-arg": "^12.0.0", + "npm-registry-fetch": "^18.0.1", + "proc-log": "^5.0.0", + "semver": "^7.3.7", + "sigstore": "^3.0.0", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.1", + "@npmcli/run-script": "^9.0.1", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "10.4.3", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "14.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "9.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "7.1.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-fetch/node_modules/minizlib": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/mkdirp": { + "version": "1.0.4", + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "11.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/minizlib": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/mkdirp": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/tar": { + "version": "7.4.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/nopt/node_modules/abbrev": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "7.0.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^8.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "7.1.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "12.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "10.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "10.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "11.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "18.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch/node_modules/minizlib": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "3.0.0", + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "7.0.3", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/package-json-from-dist": { + "version": "1.0.1", + "inBundle": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/npm/node_modules/pacote": { + "version": "21.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^10.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/path-key": { + "version": "3.1.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "1.11.1", + "inBundle": 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/npm/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/proggy": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "3.0.2", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-inflight": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "inBundle": true, "license": "MIT", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "err-code": "^2.0.2", + "retry": "^0.12.0" }, "engines": { - "node": ">= 8" + "node": ">=10" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", + "node_modules/npm/node_modules/promzard": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", "dependencies": { - "minimist": "^1.2.6" + "read": "^4.0.0" }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "inBundle": true, "bin": { - "mkdirp": "bin/cmd.js" + "qrcode-terminal": "bin/qrcode-terminal.js" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" + "node_modules/npm/node_modules/read": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "^2.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/read-package-json-fast": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "inBundle": true, "license": "MIT", "engines": { - "node": "*" + "node": ">= 4" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "node_modules/npm/node_modules/rimraf": { + "version": "5.0.10", + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/nan": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "inBundle": true, "license": "MIT", "optional": true }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "license": "MIT" + "node_modules/npm/node_modules/semver": { + "version": "7.6.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" + "node_modules/npm/node_modules/shebang-command": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "node_modules/npm/node_modules/shebang-regex": { + "version": "3.0.0", + "inBundle": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/node-abi": { - "version": "3.71.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", - "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", + "node_modules/npm/node_modules/signal-exit": { + "version": "4.1.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^3.0.0", + "@sigstore/tuf": "^3.0.0", + "@sigstore/verify": "^2.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.8.3", + "inBundle": true, "license": "MIT", "dependencies": { - "semver": "^7.3.5" + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" }, "engines": { - "node": ">=10" + "node": ">= 10.0.0", + "npm": ">= 3.0.0" } }, - "node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "license": "MIT" + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "inBundle": true, "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.5.0", + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.21", + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/sprintf-js": { + "version": "1.1.3", + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/npm/node_modules/ssri": { + "version": "12.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" }, "engines": { - "node": "4.x || >=6.0.0" + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "peerDependencies": { - "encoding": "^0.1.0" + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "engines": { + "node": ">=8" } }, - "node_modules/node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "node_modules/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" + "ansi-regex": "^5.0.1" }, - "bin": { - "node-gyp": "bin/node-gyp.js" + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">= 10.12.0" + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "9.4.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/node-gyp/node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", + "node_modules/npm/node_modules/tar": { + "version": "6.2.1", + "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=10" } }, - "node_modules/node-gyp/node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", + "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" + "minipass": "^3.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">= 8" } }, - "node_modules/node-gyp/node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", + "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" + "yallist": "^4.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=8" } }, - "node_modules/nodemailer": { - "version": "6.9.16", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", - "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", - "license": "MIT-0", + "node_modules/npm/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", "engines": { - "node": ">=6.0.0" + "node": ">=8" } }, - "node_modules/nodemon": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", - "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/nodemon/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "node_modules/npm/node_modules/tuf-js": { + "version": "3.0.1", + "inBundle": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@tufjs/models": "3.0.1", + "debug": "^4.3.6", + "make-fetch-happen": "^14.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/nodemon/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", + "node_modules/npm/node_modules/unique-filename": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "unique-slug": "^5.0.0" }, "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/nodemon/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, + "node_modules/npm/node_modules/unique-slug": { + "version": "5.0.0", + "inBundle": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "imurmurhash": "^0.1.4" }, "engines": { - "node": ">= 6" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "inBundle": true, "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", "engines": { - "node": ">=4" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "node_modules/npm/node_modules/walk-up-path": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/which": { + "version": "5.0.0", + "inBundle": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" }, "engines": { - "node": "*" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/nodemon/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "node_modules/npm/node_modules/which/node_modules/isexe": { + "version": "3.1.1", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "8.1.0", + "inBundle": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/nodemon/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, + "node_modules/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "inBundle": true, "license": "MIT", "dependencies": { - "picomatch": "^2.2.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=8.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, + "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", - "dependencies": { - "abbrev": "1" + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" }, - "bin": { - "nopt": "bin/nopt.js" + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "inBundle": true, + "license": "MIT", "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "6.0.0", + "inBundle": true, "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "dev": true, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, "license": "ISC" }, "node_modules/npmlog": { @@ -4925,9 +10488,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4967,28 +10530,21 @@ } }, "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "license": "MIT", "dependencies": { - "mimic-function": "^5.0.0" + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=18" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "license": "MIT", - "peer": true - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5007,97 +10563,6 @@ "node": ">= 0.8.0" } }, - "node_modules/ora": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.1.1.tgz", - "integrity": "sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ora/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -5178,11 +10643,74 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", + "integrity": "sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/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/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -5191,6 +10719,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5242,11 +10788,16 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -5262,42 +10813,89 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/playwright": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", - "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", "dependencies": { - "playwright-core": "1.49.1" + "find-up": "^4.0.0" }, - "bin": { - "playwright": "cli.js" + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=18" + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" }, - "optionalDependencies": { - "fsevents": "2.3.2" + "engines": { + "node": ">=8" } }, - "node_modules/playwright-core": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", - "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" }, "engines": { - "node": ">=18" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/prebuild-install": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", - "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", @@ -5305,7 +10903,7 @@ "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", + "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", @@ -5346,6 +10944,43 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -5381,28 +11016,108 @@ "node": ">= 6" } }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/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": ">= 0.10" + "node": ">= 14" } }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", "engines": { - "node": ">= 0.10" + "node": ">=12" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -5430,6 +11145,61 @@ "node": ">=6" } }, + "node_modules/puppeteer": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.2.0.tgz", + "integrity": "sha512-z8vv7zPEgrilIbOo3WNvM+2mXMnyM9f4z6zdrB88Fzeuo43Oupmjrzk3EpuvuCtyK0A7Lsllfx7Z+4BvEEGJcQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.7.1", + "chromium-bidi": "1.2.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1402036", + "puppeteer-core": "24.2.0", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.2.0.tgz", + "integrity": "sha512-e4A4/xqWdd4kcE6QVHYhJ+Qlx/+XpgjP4d8OwBx0DJoY/nkIRhSgYmKQnv7+XSs1ofBstalt+XPGrkaz4FoXOQ==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.7.1", + "chromium-bidi": "1.2.0", + "debug": "^4.4.0", + "devtools-protocol": "0.0.1402036", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -5520,6 +11290,13 @@ "node": ">=0.10.0" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/read-installed": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", @@ -5592,12 +11369,12 @@ } }, "node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": ">= 14.18.0" }, "funding": { "type": "individual", @@ -5627,6 +11404,15 @@ "regexp-tree": "bin/regexp-tree" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -5658,11 +11444,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -5678,34 +11486,14 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "dev": true, "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/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": ">=10" } }, "node_modules/retry": { @@ -5815,9 +11603,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6056,6 +11844,12 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -6076,6 +11870,16 @@ "dev": true, "license": "MIT" }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/slide": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", @@ -6091,7 +11895,6 @@ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "license": "MIT", - "optional": true, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -6102,7 +11905,6 @@ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "license": "MIT", - "optional": true, "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" @@ -6113,18 +11915,47 @@ } }, "node_modules/socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "license": "MIT", - "optional": true, "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" }, "engines": { - "node": ">= 10" + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, "node_modules/spdx-compare": { @@ -6169,9 +12000,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.20", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", - "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", "dev": true, "license": "CC0-1.0" }, @@ -6204,8 +12035,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause", - "optional": true + "license": "BSD-3-Clause" }, "node_modules/sqlite3": { "version": "5.1.7", @@ -6276,6 +12106,29 @@ "node": "*" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -6285,17 +12138,17 @@ "node": ">= 0.8" } }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "dev": true, + "node_modules/streamx": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", + "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { @@ -6307,6 +12160,20 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -6334,13 +12201,23 @@ } }, "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/strip-json-comments": { @@ -6356,120 +12233,84 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" }, "engines": { - "node": ">=8" + "node": ">=14.18.0" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swagger-jsdoc": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", - "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", - "license": "MIT", - "dependencies": { - "commander": "6.2.0", - "doctrine": "3.0.0", - "glob": "7.1.6", - "lodash.mergewith": "^4.6.2", - "swagger-parser": "^10.0.3", - "yaml": "2.0.0-1" - }, "bin": { - "swagger-jsdoc": "bin/swagger-jsdoc.js" + "mime": "cli.js" }, "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/swagger-jsdoc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node": ">=4.0.0" } }, - "node_modules/swagger-jsdoc/node_modules/commander": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", - "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "node_modules/supertest": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", + "integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/swagger-jsdoc/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "methods": "^1.1.2", + "superagent": "^9.0.1" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=14.18.0" } }, - "node_modules/swagger-jsdoc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", + "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": { - "brace-expansion": "^1.1.7" + "has-flag": "^4.0.0" }, "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "10.0.3" - }, "engines": { - "node": ">=10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/swagger-ui-dist": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz", - "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.3.tgz", + "integrity": "sha512-G33HFW0iFNStfY2x6QXO2JYVMrFruc8AZRX0U/L71aA7WeWfX2E5Nm8E/tsipSZJeIZZbSjUDeynLK/wcuNWIw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -6572,6 +12413,12 @@ "node": ">=10" } }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/teamcity-service-messages": { "version": "0.1.14", "resolved": "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz", @@ -6579,12 +12426,67 @@ "dev": true, "license": "MIT" }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6643,16 +12545,65 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { - "typescript": ">=4.2.0" + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } } }, "node_modules/ts-node": { @@ -6730,6 +12681,22 @@ "node": ">=10.13.0" } }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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.19.2", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", @@ -6750,21 +12717,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/tsx/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -6796,6 +12748,29 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -6809,11 +12784,17 @@ "node": ">= 0.6" } }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true, + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "devOptional": true, "license": "Apache-2.0", "peer": true, "bin": { @@ -6825,15 +12806,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.19.0.tgz", - "integrity": "sha512-Ni8sUkVWYK4KAcTtPjQ/UTiRk6jcsuDhPpxULapUDi8A/l8TSBk+t1GtJA1RsCzIJg0q6+J7bf35AwQigENWRQ==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.23.0.tgz", + "integrity": "sha512-/LBRo3HrXr5LxmrdYSOCvoAMm7p2jNizNfbIpCgvG4HMsnoprRUOce/+8VJ9BDYWW68rqIENE/haVLWPeFZBVQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.19.0", - "@typescript-eslint/parser": "8.19.0", - "@typescript-eslint/utils": "8.19.0" + "@typescript-eslint/eslint-plugin": "8.23.0", + "@typescript-eslint/parser": "8.23.0", + "@typescript-eslint/utils": "8.23.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6871,7 +12852,6 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, "license": "MIT" }, "node_modules/unique-filename": { @@ -6903,6 +12883,37 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -6935,6 +12946,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -6942,6 +12966,21 @@ "dev": true, "license": "MIT" }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -6953,15 +12992,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -6971,6 +13001,16 @@ "node": ">= 0.8" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/watskeburt": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-4.2.2.tgz", @@ -7089,25 +13129,156 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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/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/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, "license": "ISC" }, "node_modules/yaml": { - "version": "2.0.0-1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", - "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14" + } + }, + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, + "node_modules/yamljs/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/yamljs/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" } }, "node_modules/yn": { @@ -7133,34 +13304,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/z-schema": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", - "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", - "license": "MIT", - "dependencies": { - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "validator": "^13.7.0" - }, - "bin": { - "z-schema": "bin/z-schema" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "commander": "^9.4.1" - } - }, - "node_modules/z-schema/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "license": "MIT", - "optional": true, - "engines": { - "node": "^12.20.0 || >=14" + "funding": { + "url": "https://github.com/sponsors/colinhacks" } } } diff --git a/package.json b/package.json index 6b38fd68..c48ee738 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,31 @@ { "name": "dockstatapi", + "repository": "git@github.com:Its4Nik/dockstatapi.git", "version": "2.0.1", "description": "API for docker hosts using dockerode", "main": "src/server.ts", "scripts": { + "test": "NODE_ENV=testing jest -w 1 --forceExit", + "test:silent": "NODE_ENV=testing jest -w 1 --forceExit --silent", "local-env-file": "bash ./src/misc/createEnvDev.sh", - "start": "npm run local-env-file && tsx src/server.ts", - "dev": "npm run local-env-file && nodemon", - "dev:trace": "npm run local-env-file && nodemon --trace-uncaught --trace-warnings", + "start": "npm run local-env-file && NODE_ENV=production tsx src/server.ts", + "start:build": "npm run local-env-file -d && npm run build && NODE_ENV=production node dist/src/src/server.js", + "dev": "npm run local-env-file && NODE_ENV=development nodemon", + "dev:socket": "docker compose -f docker/docker-compose.dev.yaml up -d && npm run local-env-file && NODE_ENV=development nodemon ; docker compose -f docker/docker-compose.dev.yaml down", + "dev:trace": "npm run local-env-file && NODE_ENV=development nodemon --trace-uncaught --trace-warnings", "dep": "bash ./src/misc/dependencyGraphs/createDependencyGraph.sh", "dep:remove": "bash ./src/misc/removeUnusedDeps.sh && npm run dep", - "build": "npx tsc", - "build:mini": "npx tsc && bash ./src/misc/minifyDist.sh --build-only", + "build": "tsc", + "build:mini": "tsc && bash ./src/misc/minifyDist.sh --build-only", "build:docker": "docker build . -t \"dockstatapi:local\" -f ./docker/Dockerfile-dev", + "build:docker:prod": "docker build . -t \"dockstatapi:local\" -f ./docker/Dockerfile-base", "mini": "bash ./src/misc/minifyDist.sh", "docker": "docker compose -f docker/docker-compose.yaml up -d && bash ./src/misc/.tmux.sh; docker compose -f docker/docker-compose.yaml down", "docker:build": "npm run build:docker && npm run docker", - "prettier": "npx prettier -c ./src/**/*.ts --parser typescript --write && npx prettier -c ./.github/workflows/*.yaml --parser yaml --write && npx prettier -c ./**/*.md --parser markdown --write && npx prettier -c ./**/*.json --parser json --write", - "lint": "npx eslint", - "lint:fix": "npx eslint --fix", + "docker:build:prod": "npm run build:docker:prod && npm run docker", + "prettier": "prettier -c ./__tests__/*.spec.ts --parser typescript --write && prettier -c ./src/**/*.ts --parser typescript --write && prettier -c ./.github/workflows/*.yaml --parser yaml --write && prettier -c ./**/*.md --parser markdown --write && prettier -c ./**/*.json --parser json --write", + "lint": "eslint", + "lint:fix": "eslint --fix", "license": "bash ./src/misc/credits.sh", "finish": "npm run local-env-file && npm run license && npm run prettier && npm run lint" }, @@ -29,40 +36,52 @@ "bcrypt": "^5.1.1", "chokidar": "^4.0.1", "cors": "^2.8.5", + "cytoscape": "^3.30.4", + "docker-compose": "^1.1.0", "dockerode": "^4.0.2", "express": "^4.21.1", "express-rate-limit": "^7.4.1", "https": "^1.0.0", + "i": "^0.3.7", "ipaddr.js": "^2.2.0", "nodemailer": "^6.9.16", + "npm": "^11.0.0", + "puppeteer": "^24.0.0", "sqlite3": "^5.1.7", - "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", "winston": "^3.15.0", - "winston-daily-rotate-file": "^5.0.0" + "winston-daily-rotate-file": "^5.0.0", + "yamljs": "^0.3.0" }, "devDependencies": { "@eslint/js": "^9.17.0", - "@playwright/test": "^1.49.0", "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", + "@types/cytoscape": "^3.21.8", "@types/dockerode": "^3.3.31", "@types/express": "^5.0.0", "@types/express-handlebars": "^5.3.1", + "@types/jest": "^29.5.14", "@types/node": "^22.9.0", + "@types/node-fetch": "^2.6.12", "@types/nodemailer": "^6.4.17", + "@types/supertest": "^6.0.2", "@types/supports-color": "^8.1.3", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.7", + "@types/ws": "^8.5.14", + "@types/yamljs": "^0.2.34", "@typescript-eslint/eslint-plugin": "^8.18.2", "@typescript-eslint/parser": "^8.18.2", "dependency-cruiser": "^16.5.0", "eslint": "^9.17.0", "globals": "^15.14.0", + "jest": "^29.7.0", "license-checker": "^25.0.1", "nodemon": "^3.1.7", - "ora": "^8.1.1", "prettier": "^3.4.2", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "tsx": "^4.19.2", "typescript-eslint": "^8.18.2", @@ -71,5 +90,34 @@ "engines": { "npm": ">=10.8.2" }, - "repository": "git@github.com:Its4Nik/dockstatapi.git" + "jest": { + "preset": "ts-jest", + "testMatch": [ + "**/__tests__/**/*.(test|spec).ts" + ], + "testEnvironment": "node", + "transform": { + "^.+\\.(ts|tsx)$": "ts-jest" + }, + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "json", + "node" + ], + "coveragePathIgnorePatterns": [ + "/node_modules/" + ], + "moduleNameMapper": { + "^@/(.*)$": "src/$1" + }, + "transformIgnorePatterns": [ + "/node_modules/" + ], + "testPathIgnorePatterns": [ + "util" + ] + } } diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index 2c33a93e..00000000 --- a/playwright.config.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - -export default defineConfig({ - timeout: 300000, - testDir: './tests', - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - reporter: 'html', - use: { - trace: 'on-first-retry', - }, - - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - ], - - webServer: { - command: 'npm run start', - url: 'http://127.0.0.1:9876', - reuseExistingServer: true - }, -}); diff --git a/src/config/db.ts b/src/config/db.ts index edfe3832..5ed4d6a0 100644 --- a/src/config/db.ts +++ b/src/config/db.ts @@ -14,7 +14,7 @@ const db: sqlite3.Database = new sqlite3.Database(dbPath, (error: unknown) => { timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )`, () => { - logger.info("Database created / opened successfully, table is ready."); + logger.info("Database created / checked successfully, table is ready."); }, ); } diff --git a/src/config/hostsystem.ts b/src/config/hostsystem.ts index 0af379f6..87928a8e 100644 --- a/src/config/hostsystem.ts +++ b/src/config/hostsystem.ts @@ -3,7 +3,8 @@ import { VERSION, HA_MASTER, HA_UNSAFE, - TRUSTED_PROXYS, + TRUSTED_PROXIES, + LOG_LEVEL, } from "./variables"; import fs from "fs"; import logger from "../utils/logger"; @@ -16,7 +17,15 @@ const version: string = VERSION || "unknown"; const masterNode: string = HA_MASTER === "true" ? "✓" : "✗"; const unsafeSync: string = HA_UNSAFE === "true" ? "✓" : "✗"; -function writeUserConf() { +let trustedProxies: string = ""; + +if (TRUSTED_PROXIES) { + trustedProxies = TRUSTED_PROXIES; +} else { + trustedProxies = "✗"; +} + +function writeUserConf(port: number) { let previousConfig = null; let shouldRewriteConfig = false; @@ -64,6 +73,7 @@ function writeUserConf() { logger.info("-----------------------------------------"); logger.info(`Starting at : ${startDetails.startedAt}`); + logger.info(`Running env : ${process.env.NODE_ENV}`); logger.info(`Version : ${startDetails.backendVersion}`); logger.info(`Docker : ${installationDetails.inDocker}`); logger.info(`Running as : ${installationDetails.installedBy}`); @@ -71,7 +81,12 @@ function writeUserConf() { logger.info(`Arch : ${installationDetails.arch}`); logger.info(`Master node : ${masterNode}`); logger.info(`Unsafe sync : ${unsafeSync}`); - logger.info(`Proxies : ${TRUSTED_PROXYS}`); + logger.info(`Proxies : ${trustedProxies}`); + logger.info(`Log Level : ${LOG_LEVEL}`); + logger.info(`Server : http://localhost:${port}`); + if (process.env.NODE_ENV !== "production") { + logger.info(`Swagger-UI : http://localhost:${port}/api-docs`); + } logger.info("-----------------------------------------"); } diff --git a/src/config/initFiles.ts b/src/config/initFiles.ts index 008749cb..7524907c 100644 --- a/src/config/initFiles.ts +++ b/src/config/initFiles.ts @@ -3,6 +3,7 @@ import logger from "../utils/logger"; import { atomicWrite } from "../utils/atomicWrite"; const files = [ + { path: "./src/data/highAvailability.json", content: "{}" }, { path: "./src/data/password.json", content: JSON.stringify( diff --git a/src/config/stacks.ts b/src/config/stacks.ts new file mode 100644 index 00000000..def75dcb --- /dev/null +++ b/src/config/stacks.ts @@ -0,0 +1,260 @@ +import logger from "../utils/logger"; +import fs from "fs"; +import path from "path"; +import YAML from "yamljs"; +import { DockerComposeFile } from "../typings/dockerCompose"; +import { dockerStackProperty, dockerStackEnv } from "../typings/dockerStackEnv"; +import { stackConfig } from "../typings/stackConfig"; +import { validate } from "../handlers/stack"; +import { atomicWrite } from "../utils/atomicWrite"; +import { AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT } from "./variables"; + +const nameRegex = /^[A-Za-z0-9_-]+$/; +const stackRootFolder = "./stacks"; +const configFilePath = `${stackRootFolder}/.config.json`; + +async function getStackCompose(name: string) { + try { + await validate(name); + const stackCompose = `${stackRootFolder}/${name}/docker-compose.yaml`; + + return YAML.parse(fs.readFileSync(stackCompose, "utf-8")); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + +async function getStackConfig(): Promise { + try { + return fs.readFileSync(configFilePath, "utf-8"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + +async function createStack( + name: string, + content: DockerComposeFile, + override: boolean, +) { + try { + if (!name) { + const errorMsg = "Name required"; + logger.error(errorMsg); + throw new Error(errorMsg); + } + + if (!nameRegex.test(name)) { + const errorMsg = "Name does not match [A-Za-z0-9_-]"; + logger.error(errorMsg); + throw new Error(errorMsg); + } + + if (!content) { + const errorMsg = "Data for this stack is required"; + logger.error(errorMsg); + throw new Error(errorMsg); + } + + const stackFolderPath = `${stackRootFolder}/${name}`; + + if (!fs.existsSync(stackFolderPath)) { + fs.mkdirSync(stackFolderPath, { recursive: true }); + logger.debug(`Created stack folder at ${stackFolderPath}`); + } + + updateConfigFile(name); + + let yamlContent = ""; + let environmentFileData: dockerStackEnv = { environment: [] }; + if (AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT == "true" && override == false) { + logger.debug("AEFM is activated"); + const { cleanCompose, envSchema } = extractAndRemoveEnv(content); + yamlContent = YAML.stringify(cleanCompose, 10, 2); + environmentFileData = envSchema; + + await writeEnvFile(name, environmentFileData); + } else { + yamlContent = YAML.stringify(content, 10, 2); + } + + const filePath = `${stackFolderPath}/docker-compose.yaml`; + atomicWrite(filePath, yamlContent); + logger.debug(`Stack content written to ${filePath}`); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + +function updateConfigFile(stackName: string) { + try { + let config: stackConfig = { stacks: [] }; + if (fs.existsSync(configFilePath)) { + const configData = fs.readFileSync(configFilePath, "utf-8"); + config = JSON.parse(configData); + } + + const stacks = config.stacks || []; + + if (!stacks.includes(stackName)) { + stacks.push(stackName); + } + + const updatedConfig = { stacks }; + atomicWrite(configFilePath, JSON.stringify(updatedConfig, null, 2)); + logger.debug(`Updated .config.json with stack name: ${stackName}`); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(`Error updating .config.json: ${errorMsg}`); + throw new Error(errorMsg); + } +} + +async function writeEnvFile( + name: string, + data: dockerStackEnv, +): Promise { + try { + await validate(name); + + if (!nameRegex.test(name)) { + const sanitizedStackName = name.replace(/\n|\r/g, ""); + const errorMsg = `Invalid stack name: ${sanitizedStackName}`; + logger.error(errorMsg); + return false; + } + + const dockerEnvPath = path.resolve(stackRootFolder, name, "docker.env"); + const dockerEnvPathBak = path.resolve( + stackRootFolder, + name, + ".docker.env.bak", + ); + + if ( + !dockerEnvPath.startsWith(path.resolve(stackRootFolder)) || + !dockerEnvPathBak.startsWith(path.resolve(stackRootFolder)) + ) { + const sanitizedStackName = name.replace(/\n|\r/g, ""); + const errorMsg = `Path traversal attempt detected: ${sanitizedStackName}`; + logger.error(errorMsg); + return false; + } + + const variableNames = data.environment.map(({ name }) => name); + const duplicateVars = variableNames.filter( + (item, index) => variableNames.indexOf(item) !== index, + ); + + if (duplicateVars.length > 0) { + const duplicatesList = duplicateVars.join(", "); + const sanitizedDuplicatesList = duplicatesList.replace(/\n|\r/g, ""); + const errorMsg = `Duplicate environment variables detected: ${sanitizedDuplicatesList}`; + logger.error(errorMsg); + return false; + } + + const envFileContent = data.environment + .map(({ name, value }) => `${name}="${value}"`) + .join("\n"); + + if (fs.existsSync(dockerEnvPath)) { + logger.debug("Creating a local backup"); + const previousData = fs.readFileSync(dockerEnvPath); + atomicWrite(dockerEnvPathBak, previousData); + } + + atomicWrite(dockerEnvPath, envFileContent); + return true; + } catch (error: unknown) { + const errorMsg = ( + error instanceof Error ? error.message : String(error) + ).replace(/\n|\r/g, ""); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + +async function getEnvFile(name: string) { + await validate(name); + const dockerEnvPath = path.resolve(stackRootFolder, name, "docker.env"); + if (!dockerEnvPath.startsWith(path.resolve(stackRootFolder))) { + throw new Error("Invalid path"); + } + + if (fs.existsSync(dockerEnvPath)) { + const data = fs.readFileSync(dockerEnvPath, "utf-8"); + + const environment: dockerStackProperty[] = data + .split("\n") + .filter((line) => line.trim() !== "" && line.includes("=")) + .map((line) => { + const [name, ...valueParts] = line.split("="); + const value = valueParts.join("=").replace(/^"|"$/g, ""); + return { name: name.trim(), value: value.trim() }; + }); + + return { environment }; + } else { + return null; + } +} + +function extractAndRemoveEnv(data: DockerComposeFile): { + cleanCompose: DockerComposeFile; + envSchema: dockerStackEnv; +} { + const environment: dockerStackProperty[] = []; + const envCount: Record = {}; + + for (const [, service] of Object.entries(data.services)) { + if (service.environment) { + for (const key of Object.keys(service.environment)) { + envCount[key] = (envCount[key] || 0) + 1; + } + } + } + + for (const [, service] of Object.entries(data.services)) { + if (service.environment) { + const remainingEnvironment: Record = {}; + + for (const [key, value] of Object.entries(service.environment)) { + if (envCount[key] === 1) { + environment.push({ name: key, value }); + } else { + remainingEnvironment[key] = value; + } + } + + service.environment = remainingEnvironment; + + if (Object.keys(service.environment).length === 0) { + delete service.environment; + } + } + + if (!service.env_file) { + service.env_file = ["./docker.env"]; + } + } + + return { + cleanCompose: data, + envSchema: { environment }, + }; +} + +export { + createStack, + getStackConfig, + getStackCompose, + writeEnvFile, + getEnvFile, +}; diff --git a/src/config/swagger.yaml b/src/config/swagger.yaml new file mode 100644 index 00000000..9a1d50fb --- /dev/null +++ b/src/config/swagger.yaml @@ -0,0 +1,2095 @@ +openapi: "3.0.0" + +security: + - passwordAuth: [] + +info: + title: "DockStatAPI" + version: "2.0.1" + externalDocs: + description: DockStat(API) Wiki + url: https://outline.itsnik.de/s/dockstat + license: + name: BSD-3-Clause + url: https://github.com/Its4Nik/dockstatapi/tree/main?tab=BSD-3-Clause-1-ov-file#readme + contact: + email: info@itsnik.de + description: |- + ![DockStat](https://github.com/Its4Nik/dockstatapi/blob/dev/.github/DockStat-dark.png?raw=true) + + # Pipelines + + [![Docker Image CI](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/build-image.yml?branch=main&label=Docker%20Image%20CI&style=for-the-badge&logo=docker)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml) + [![Validation](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/validation.yml?branch=dev&label=Validation&style=for-the-badge&logo=checkmarx)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) + + # Feature List: + + - Swagger API Documentation + - Database (Keeps data for 24 hours max) + - Advanced authentication using hashes and salt + - `http` API to configure the backend + - Multi-arch docker builds (using buildx github action) + - Advanced security through middlewares: rate-limiting and authentication + - Multi Arch Docker builds through docker buildx + - High Availability using single master and unlimited worker nodes! + +

+ Your container graph + [Interactive Graph](http://localhost:9876/graph) + + [Raw image](http://localhost:9876/graph/image) + + --- + + ![Your container graph](http://localhost:9876/graph/image) +
+ + # 🔗 DockStatAPI v2 Documentation + + _⚠️ = Deprecation warning_ + + - [Introduction](https://outline.itsnik.de/s/dockstat) + + - [DockstatAPI v2](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v2-XRMDKRqMIg) + + - [API reference](https://outline.itsnik.de/s/dockstat/doc/api-reference-1PTxqx1MQ6) + - [How dependency graphs are made](https://outline.itsnik.de/s/dockstat/doc/how-the-dependecy-graphs-are-made-svuZbEHH9g) + + - [DockStat v1](https://outline.itsnik.de/s/dockstat/doc/dockstat-v1-zVaFS4zROI) + + - [⚠️ Customisation](https://outline.itsnik.de/s/dockstat/doc/customization-PiBz4OpQIZ) + - [⚠️ Themes](https://outline.itsnik.de/s/dockstat/doc/themes-BFhN6ZBbYx) + - [⚠️ Installation](https://outline.itsnik.de/s/dockstat/doc/installation-DaO99bB86q) + + - [⚠️ DockStatAPI v1](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v1-jLcVCfPNmS) + - [⚠️ Integrations](https://outline.itsnik.de/s/dockstat/doc/integrations-Agq1oL6HxF) + - [⚠️ Backend API reference](https://outline.itsnik.de/s/dockstat/doc/backend-api-reference-YzcBbDvY33) + +tags: + - name: Authentication + description: Routes to setup / configure authentication + + - name: Configuration + description: Configuring the backend + + - name: Database queries + description: Queries made against the SQLite database + + - name: "Frontend Configuration" + description: Backend routes to configure the integrated "frontend service" + + - name: Miscellaneous + description: Some "random" routes which still can be useful + + - name: High availability + description: High availability routes, mainly used by HA sync + + - name: Notification Service + description: Routes to configure the notification service + + - name: Stacks + description: Management of the Stack module + +servers: + - url: http://localhost:9876 + description: "Your DockStatAPI instance" + +paths: + # ------------------------------ + # Authentication setup: + /auth/enable: + post: + tags: + - "Authentication" + summary: Enable authentication for every route + operationId: enableAuth + parameters: + - name: password + in: query + required: true + explode: true + schema: + type: string + default: super-secret + responses: + "200": + description: Success - Successfully enabled authentication + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Authentication enabled successfully" + + "403": + description: Error - Password is required / Authentication is already enabled + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /auth/disable: + post: + tags: + - "Authentication" + summary: Disable authentication for every route + operationId: disableAuth + parameters: + - name: password + in: query + required: true + explode: true + schema: + type: string + default: super-secret + responses: + "200": + description: Succes - Succesfully disabled authentication + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Authentication disabled successfully" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + # ------------------------------ + # Database queries: + /data/latest: + get: + tags: + - "Database queries" + summary: Fetched the last added entry from the Database and provides it via a JSON output + operationId: getLatestData + responses: + "200": + description: Succes - Successfully fetched the database + content: + application/json: + schema: + $ref: "#/components/schemas/ServerContainers" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "404": + description: Error - No entries found inside database + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /data/all: + get: + tags: + - "Database queries" + summary: Provides all database entries with an index starting from 0 + operationId: getAllData + responses: + "200": + description: Succes - Successfully fetched the database + content: + application/json: + schema: + $ref: "#/components/schemas/IndexedServerContainers" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "404": + description: Error - No entries found inside database + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /data/clear: + delete: + tags: + - "Database queries" + summary: Deletes all database entries + operationId: dataClear + responses: + "200": + description: Succes - Successfully cleared the database + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Successfully cleared the database" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + # ------------------------------ + # Configuration: + /api/hosts: + get: + tags: + - "Configuration" + summary: Retrieves the configured name of all added Hosts + operationId: getHosts + responses: + "200": + description: Succes - Successfully fetched all configured hosts + content: + application/json: + schema: + type: array + example: '[ "Host-1", "Host-2" ]' + + "400": + description: Error - No hosts defined, please add a host via /conf/addHost + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /api/host/{hostName}/stats: + get: + tags: + - "Configuration" + summary: Shows general information about the target host, like dockeer engine version + operationId: getHostInfo + parameters: + - name: hostName + in: path + description: Hostname of the target host + required: true + schema: + type: string + responses: + "200": + description: Succes - Successfully fetched info about target host + content: + application/json: + schema: + $ref: "#/components/schemas/HostInfo" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "404": + description: Error - No Host found + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /api/system: + get: + tags: + - "Configuration" + summary: Fetched the installation details of this DockStatAPI instance + operationId: getSystem + responses: + "200": + description: Succes - Fetched system configuration + content: + application/json: + schema: + type: object + properties: + installedAt: + type: string + format: date-time + example: "2024-12-25T19:20:02.418Z" + backendVersion: + type: string + example: "2.0.1" + inDocker: + type: boolean + example: false + installedBy: + type: string + example: "user" + platform: + type: string + example: "linux" + arch: + type: string + example: "x64" + "400": + description: Error - Received empty configuration + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /api/config: + get: + tags: + - "Configuration" + summary: Retrieves information about the configured hosts + operationId: getConfig + responses: + "200": + description: Succes - Fetched system configuration + content: + application/json: + schema: + type: object + properties: + hosts: + type: array + items: + type: object + properties: + name: + type: string + example: "Host-1" + url: + type: string + example: "192.168.2.12" + port: + type: string + example: "2375" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /api/frontend-config: + get: + tags: + - "Configuration" + summary: Fetches the "Frontend Configuration" => Used in the DockStat frontend + operationId: getFrontendConfig + responses: + "200": + description: Succes - Fetched "Frontend Configuration" + content: + application/json: + schema: + $ref: "#/components/schemas/FrontendConfig" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /api/current-schedule: + get: + tags: + - "Configuration" + summary: Shows the current configured schedule (for fetching data) in seconds + operationId: getSchedule + responses: + "200": + description: Succes - Fetched schedule + content: + application/json: + schema: + type: object + properties: + interval: + type: integer + example: 600 + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /api/status: + get: + tags: + - "Miscellaneous" + summary: Pings all hosts to check reachability + operationId: getStatus + responses: + "200": + description: Succes - Gathered Status + content: + application/json: + schema: + $ref: "#/components/schemas/ApiStatus" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /api/containers: + get: + tags: + - "Miscellaneous" + summary: Fetched all container data directly from the host without reading from the database + operationId: getContainers + responses: + "200": + description: Succes - Fetched all container statistics + content: + application/json: + schema: + $ref: "#/components/schemas/ServerContainers" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + # ------------------------------ + # High availability: + /ha/config: + get: + tags: + - "High availability" + summary: Get the current high availability config + operationId: getHaConfig + responses: + "200": + description: Succes - Fetched high availability config + content: + application/json: + schema: + $ref: "#/components/schemas/HaConfig" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + + /ha/sync: + post: + tags: + - "High availability" + deprecated: true + summary: This route is not deprecated, but only used by the high availability feature + operationId: syncHa + responses: + "200": + description: Succes - Synchronized successfully + "400": + description: Error - `files` object is missing or invalid + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + + /ha/prepare-sync: + get: + tags: + - "High availability" + deprecated: true + summary: This route is not deprecated, but only used by the high availability feature + operationId: syncPrepare + responses: + "200": + description: Succes - Prepared all files for syncing + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + + # ------------------------------ + # Notification Service: + /notification-service/get-template: + get: + tags: + - "Notification Service" + summary: Fetches the current template for the notification service + operationId: getNsTemplate + responses: + "200": + description: Success - Fetched notification template + content: + application/json: + schema: + $ref: "#/components/schemas/Notification-Template" + "400": + description: Error - Error while reading file (see server logs) + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /notification-service/set-template: + post: + tags: + - "Notification Service" + - "Configuration" + summary: Update the current notification template + operationId: setNsTemplate + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Notification-Template" + responses: + "200": + description: Success - Template updated successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Template updated successfully." + "400": + description: Error - Invalid input format. Expected JSON with a 'text' field + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Invalid input format. Expected JSON with a 'text' field" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /notification-service/test/{type}/{containerId}: + post: + tags: + - "Notification Service" + summary: Test a specific type of notification using real data + operationId: testNs + parameters: + - in: path + name: type + required: true + schema: + type: string + description: The desired notification to test + + - in: path + name: containerId + required: true + schema: + type: string + description: A real container ID is needed to test templating functionality + responses: + "200": + description: Success - Sent test notification + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Sent test notification" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + # ------------------------------ + # Configuration: + /conf/addHost: + put: + tags: + - "Configuration" + summary: Adds a new host to the configuration and starts querying it + operationId: addHost + parameters: + - name: name + in: query + required: true + description: A name for the new host + - name: url + in: query + required: true + description: The target IP or dns entry + - name: port + in: query + required: true + description: The targets port on which Docker-Socket-Proxy runs + responses: + "200": + description: Success - Host added successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Host added successfully" + "400": + description: Error - Name, Port, and URL are required + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Name, Port, and URL are required" + "401": + description: Host already exists + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Host already exists" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /conf/removeHost: + delete: + tags: + - "Configuration" + summary: Removes an host from the config + operationId: removeHost + parameters: + - name: hostName + in: query + required: true + description: "The name of the to-be-removed-Host" + responses: + "200": + description: Success - Host removed successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Host removed successfully" + "401": + description: Error - Host name is required + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Host name is required" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "404": + description: Error - Host not found + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Host not found" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /conf/scheduler: + tags: + - "Configuration" + summary: Adjust the scheduler timing + operationId: adjustSchedule + parameters: + - name: interval + in: query + required: true + description: "Adjust the schedule timing (in seconds)" + responses: + "200": + description: Success - Timing updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Updated interval" + "401": + description: Error - Interval must be between 5 minutes and 6 hours + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Interval must be between 5 minutes and 6 hours." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + # ------------------------------ + # Frontend routes: + /frontend/show/{containerName}: + post: + tags: + - "Frontend Configuration" + operationId: frShowCon + summary: Set `hide` to false for the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to unhide + responses: + "200": + description: Success - now showing the container + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Container unhidden successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/hide/{containerName}: + delete: + tags: + - "Frontend Configuration" + operationId: frHideCon + summary: Set `hide` to true for the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to unhide + responses: + "200": + description: Success - now hiding the container + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Hid container succesfully" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/tag/{containerName}/{tag}: + post: + tags: + - "Frontend Configuration" + operationId: frTagCon + summary: Add a tag to the tag array for the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to add a tag to + - name: tag + in: path + schema: + type: string + required: true + description: The name of the tag to add + responses: + "200": + description: Success - Tag added successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Tag added successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/remove-tag/{containerName}/{tag}: + delete: + tags: + - "Frontend Configuration" + operationId: frRmTagCon + summary: Remove the specified tag from the tag array for the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to remove a tag from + - name: tag + in: path + schema: + type: string + required: true + description: The name of the tag to remove + responses: + "200": + description: Success - Tag removed successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Tag removed successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/pin/{containerName}: + post: + tags: + - "Frontend Configuration" + operationId: frPinCon + summary: Set `pinned` to true for the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to pin + responses: + "200": + description: Success - Container pinned successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Container pinned successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/unpin/{containerName}: + delete: + tags: + - "Frontend Configuration" + operationId: frRmPinCon + summary: Set `pinned` to false for the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to unpin + responses: + "200": + description: Success - Container unpinned successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Container unpinned successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/add-link/{containerName}/{link}: + post: + tags: + - "Frontend Configuration" + operationId: frAddLinkCon + summary: Add a link to the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to add a link to + - name: link + in: path + schema: + type: URI + required: true + allowReserved: false + description: The URI of the link (please use Uniform Resource Identifier format) + responses: + "200": + description: Success - Link added to container successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Link added successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/remove-link/{containerName}: + delete: + tags: + - "Frontend Configuration" + operationId: frRmLinkCon + summary: Remove a link to the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to remove a link from + responses: + "200": + description: Success - Link removed from container successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Link removed successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/add-icon/{containerName}/{icon}/{useCustomIcon}: + post: + tags: + - "Frontend Configuration" + operationId: frAddIcon + summary: Add an icon (path) to the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to add an icon to + - name: icon + in: path + schema: + type: string + required: true + description: The name of the icon file + - name: useCustomIcon + in: path + schema: + type: boolean + required: false + description: If the icon is a custom icon or not + responses: + "200": + description: Success - Icon added to container successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Icon added successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /frontend/remove-icon/{containerName}: + delete: + tags: + - "Frontend Configuration" + operationId: frRmIcon + summary: Remove an icon from the specified container + parameters: + - name: containerName + in: path + schema: + type: string + required: true + description: The name of the container to remove an icon from + responses: + "200": + description: Success - Icon removed from container successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Icon removed successfully." + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + # ------------------------------ + # Stack management + /stacks/create/{name}: + post: + tags: + - "Stacks" + operationId: createStack + summary: Creates a docker-compose file inside the stack name directory + requestBody: + required: true + content: + application/json: + schema: + type: string + description: Your docker-compose.yaml contents + parameters: + - name: name + in: path + schema: + type: string + required: true + description: The name of the stack + responses: + "200": + description: Success - Stack created + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Stack created" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /stacks/start/{name}: + post: + tags: + - "Stacks" + operationId: startStack + summary: Starts the defined stack + parameters: + - name: name + in: path + schema: + type: string + required: true + description: The name of the stack + responses: + "200": + description: Success - Stack started + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Stack created" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /stacks/stop/{name}: + post: + tags: + - "Stacks" + operationId: stopStack + summary: Stops the defined stack + parameters: + - name: name + in: path + schema: + type: string + required: true + description: The name of the stack + responses: + "200": + description: Success - Stack stopped + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Stack stopped" + + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /stacks/get/{name}: + get: + tags: + - "Stacks" + operationId: getStack + summary: Get the docker-compose.yaml (as JSON) from the defined stack + parameters: + - name: name + in: path + schema: + type: string + required: true + description: The name of the stack + responses: + "200": + description: Success - Stack fetched + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /stacks/set-env/{name}: + post: + tags: + - "Stacks" + operationId: setStackEnv + summary: Set the docker.env (as JSON) from the defined stack + requestBody: + required: true + content: + application/json: + schema: + type: string + description: Your docker.env contents + parameters: + - name: override + in: query + required: false + description: Whether to override (true) the automatic environment file management (boolean value) + - name: name + in: path + schema: + type: string + required: true + description: The name of the stack + responses: + "200": + description: Success - Stack environment set + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + + /stacks/get-env/{name}: + get: + tags: + - "Stacks" + operationId: getStackEnv + summary: Get the docker.env (as JSON) from the defined stack + parameters: + - name: name + in: path + schema: + type: string + required: true + description: The name of the stack + responses: + "200": + description: Success - Stack config fetched + "403": + description: Error - Password is required + content: + application/json: + schema: + $ref: "#/components/schemas/403" + "500": + description: Error - Critical Error, please see the server's logs + content: + application/json: + schema: + $ref: "#/components/schemas/500" + "503": + description: Error - The high-availability lock is currently active, please try again later + content: + application/json: + schema: + $ref: "#/components/schemas/503" + +# ------------------------------ +components: + securitySchemes: + passwordAuth: + type: apiKey + in: header + name: x-password + description: Password required for authentication + + schemas: + Notification-Template: + type: object + properties: + text: + type: string + example: "{{container}} on {{host}} is {{state}}" + + IndexedServerContainers: + type: object + properties: + "0": + type: object + properties: + Host-1: + type: array + items: + $ref: "#/components/schemas/Container" + additionalProperties: false + + ServerContainers: + type: object + properties: + Host-1: + type: array + items: + $ref: "#/components/schemas/Container" + additionalProperties: false + + Container: + type: object + properties: + name: + type: string + description: The name of the container. + example: "Container-1" + id: + type: string + description: The unique identifier of the container. + example: "a84ca83bb0e7f8c24fe472b9164d40a4bae518ece8369e6776f722b81dd65bcf" + hostName: + type: string + description: The hostname of the server. + example: "Host-1" + state: + type: string + description: The current state of the container. + example: "running" + cpu_usage: + type: number + description: The CPU usage of the container in arbitrary units. + example: 625185.1851851852 + mem_usage: + type: integer + description: Memory usage in bytes. + example: 359899136 + mem_limit: + type: integer + description: Memory limit in bytes. + example: 8127893504 + net_rx: + type: integer + description: Total network received in bytes. + example: 11004185462 + net_tx: + type: integer + description: Total network transmitted in bytes. + example: 9950013623 + current_net_rx: + type: integer + description: Current network received in bytes. + example: 11004185462 + current_net_tx: + type: integer + description: Current network transmitted in bytes. + example: 9950013623 + networkMode: + type: string + description: The network mode of the container. + example: "docker_default" + + HostInfo: + type: object + properties: + hostName: + type: string + example: "Host-1" + info: + type: object + properties: + ID: + type: string + format: uuid + example: "32b5fad9-9b12-48b0-9ce7-178f2886ad60" + Containers: + type: integer + example: 8 + ContainersRunning: + type: integer + example: 8 + ContainersPaused: + type: integer + example: 0 + ContainersStopped: + type: integer + example: 0 + Images: + type: integer + example: 7 + OperatingSystem: + type: string + example: "Ubuntu 24.04 LTS" + KernelVersion: + type: string + example: "6.8.0-38-generic" + Architecture: + type: string + example: "x86_64" + MemTotal: + type: integer + example: 8127893504 + NCPU: + type: integer + example: 4 + version: + type: object + properties: + Components: + type: object + properties: + Engine: + type: string + example: "27.1.1" + containerd: + type: string + example: "1.7.19" + runc: + type: string + example: "1.7.19" + docker-init: + type: string + example: "0.19.0" + + Frontend: + type: object + properties: + name: + type: string + description: The name of the container + hidden: + type: boolean + description: Whether the container is hidden + tags: + type: array + items: + type: string + description: List of tags associated with the container + link: + type: string + format: uri + description: A link associated with the container + icon: + type: string + description: Icon for the container + pinned: + type: boolean + description: Whether the container is pinned + required: + - name + + FrontendConfig: + type: array + items: + $ref: "#/components/schemas/Frontend" + + ApiStatus: + type: object + properties: + ApiReachable: + type: boolean + description: Whether the API is reachable + online: + type: object + description: Status of individual services keyed by their names + properties: + Host-1: + type: boolean + Host-2: + type: boolean + required: + - ApiReachable + - online + + HaConfig: + type: object + properties: + active: + type: boolean + description: Whether High availability is active or nots + master: + type: boolean + description: Whether this node is the master node + nodes: + type: array + items: + type: string + format: hostname + description: List of nodes in the cluster, specified by hostname or IP with port + required: + - active + - master + - nodes + + 401: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Invalid password" + + 403: + type: object + properties: + status: + type: string + example: "denied" + message: + type: string + example: "Password required" + + 500: + type: object + properties: + status: + type: string + example: "critical" + message: + type: string + example: "Please see the server logs for more info" + + 503: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: "Service unavailable. The high-availability lock is currently active. Please try again later." diff --git a/src/config/swaggerConfig.ts b/src/config/swaggerConfig.ts index cab967f8..39c074a6 100644 --- a/src/config/swaggerConfig.ts +++ b/src/config/swaggerConfig.ts @@ -1,53 +1,10 @@ -const options: { - definition: { - failOnErrors: boolean; - openapi: string; - info: { - title: string; - version: string; - description: string; - }; - components: { - securitySchemes: { - passwordAuth: { - type: string; - in: string; - name: string; - description: string; - }; - }; - }; - security: Array<{ - passwordAuth: unknown[]; - }>; - }; - apis: string[]; -} = { - definition: { - failOnErrors: true, - openapi: "3.0.0", - info: { - title: "DockStatAPI", - version: "2", - description: "An API used to query muliple docker hosts", - }, - components: { - securitySchemes: { - passwordAuth: { - type: "apiKey", - in: "header", - name: "x-password", - description: "Password required for authentication", - }, - }, - }, - security: [ - { - passwordAuth: [], - }, - ], +import { SwaggerOptions } from "swagger-ui-express"; +import { css } from "./swaggerTheme"; + +export const options: SwaggerOptions = { + swaggerOptions: { + tryItOutEnabled: true, }, - apis: ["./src/routes/*/*.ts"], + customCss: css, + explorer: false, }; - -export default options; diff --git a/src/config/swaggerTheme.ts b/src/config/swaggerTheme.ts new file mode 100644 index 00000000..d8a879c9 --- /dev/null +++ b/src/config/swaggerTheme.ts @@ -0,0 +1,6 @@ +export const css = ` + +.swagger-ui .topbar { + display: none +} +`; diff --git a/src/config/variables.ts b/src/config/variables.ts index 26a522be..37c67a23 100644 --- a/src/config/variables.ts +++ b/src/config/variables.ts @@ -3,7 +3,7 @@ import vars from "../data/variables.json"; export const { VERSION, RUNNING_IN_DOCKER, - TRUSTED_PROXYS, + TRUSTED_PROXIES, HA_MASTER, HA_MASTER_IP, HA_NODE, @@ -21,4 +21,6 @@ export const { TELEGRAM_CHAT_ID, WHATSAPP_API_URL, WHATSAPP_RECIPIENT, + AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT, + LOG_LEVEL, } = vars; diff --git a/src/controllers/containerController.ts b/src/controllers/containerController.ts index 61745e17..2883dad9 100644 --- a/src/controllers/containerController.ts +++ b/src/controllers/containerController.ts @@ -1,4 +1,4 @@ -import getDockerClient from "../utils/dockerClient"; +import { getDockerClient } from "../utils/dockerClient"; import logger from "../utils/logger"; import { Request, Response } from "express"; import { createResponseHandler } from "../handlers/response"; diff --git a/src/controllers/fetchData.ts b/src/controllers/fetchData.ts index 07438ec2..06e52a93 100644 --- a/src/controllers/fetchData.ts +++ b/src/controllers/fetchData.ts @@ -1,5 +1,5 @@ import db from "../config/db"; -import fetchAllContainers from "../utils/containerService"; +import { fetchAllContainers } from "../utils/containerService"; import logger from "../utils/logger"; import fs from "fs"; import { atomicWrite } from "../utils/atomicWrite"; @@ -68,9 +68,8 @@ const fetchData = async (): Promise => { logger.info("No state change detected, notifications not triggered."); } } catch (error: unknown) { - logger.error( - `Error fetching data: ${JSON.stringify(error)} \nStack trace: ${(error as Error).stack}`, - ); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } }; diff --git a/src/controllers/frontendConfiguration.ts b/src/controllers/frontendConfiguration.ts index e8e035c1..ed4e59dd 100644 --- a/src/controllers/frontendConfiguration.ts +++ b/src/controllers/frontendConfiguration.ts @@ -23,8 +23,8 @@ async function hideContainer(containerName: string) { await saveData(data); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -41,8 +41,8 @@ async function unhideContainer(containerName: string) { cleanupData(); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -66,8 +66,8 @@ async function addTagToContainer(containerName: string, tag: string) { await saveData(data); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -86,8 +86,8 @@ async function removeTagFromContainer(containerName: string, tag: string) { cleanupData(); } } catch (error: unknown) { - logger.error(error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -108,8 +108,8 @@ async function pinContainer(containerName: string) { await saveData(data); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -126,8 +126,8 @@ async function unpinContainer(containerName: string) { cleanupData(); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -149,8 +149,8 @@ async function setLink(containerName: string, link: string) { await saveData(data); } } catch (error: unknown) { - logger.error(error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } else { logger.error(`Provided link is not valid: ${link}`); @@ -171,8 +171,8 @@ async function removeLink(containerName: string) { cleanupData(); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -201,8 +201,8 @@ async function setIcon(containerName: string, icon: string, custom: boolean) { await saveData(data); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -219,8 +219,8 @@ async function removeIcon(containerName: string) { cleanupData(); } } catch (error: unknown) { - logger.error(error as Error); - throw new Error(error as string); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -252,7 +252,8 @@ async function saveData(data: FrontendConfig) { ); logger.info("Succesfully wrote to file"); } catch (error: unknown) { - logger.error(error as Error); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -277,7 +278,8 @@ async function cleanupData() { await saveData(cleanedData); } catch (error: unknown) { - logger.error(error as Error); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts index 3e61b16f..45db9d7b 100644 --- a/src/controllers/highAvailability.ts +++ b/src/controllers/highAvailability.ts @@ -57,9 +57,9 @@ async function acquireLock(): Promise { try { atomicWrite(lockFilePath, "locked", { exclusive: true }); logger.debug("Lock acquired."); - } catch (error) { - logger.error(`Error acquiring lock: ${(error as Error).message}`); - throw new Error("Failed to acquire lock."); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -69,8 +69,9 @@ async function releaseLock(): Promise { await fs.promises.unlink(lockFilePath); logger.debug("Lock released."); } - } catch (error) { - logger.error(`Error releasing lock: ${(error as Error).message}`); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -88,8 +89,9 @@ async function writeConfig( await fs.promises.writeFile(filePath, jsonData); logger.debug(`${filePath} has been written.`); - } catch (error) { - logger.error(`Error writing config: ${(error as Error).message}`); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } finally { await releaseLock(); } @@ -104,7 +106,8 @@ async function readConfig(): Promise { ); return data; } catch (error: unknown) { - logger.error(`Error reading HA-Config: ${(error as Error).message}`); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); return null; } finally { await releaseLock(); @@ -118,8 +121,9 @@ async function prepareFilesForSync(): Promise> { const content = await fs.promises.readFile(filePath, "utf-8"); fileData[filePath] = content; } - } catch (error) { - logger.error(`Error preparing files for sync: ${(error as Error).message}`); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } return fileData; } @@ -147,8 +151,9 @@ async function checkApiReachable(node: string): Promise { logger.error(`Node ${node} is not reachable. ApiReachable: false`); return false; } - } catch (error) { - logger.error(`Error reaching node ${node}: ${(error as Error).message}`); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); return false; } } @@ -229,7 +234,7 @@ async function startMasterNode() { ? HA_NODE.split(",").reduce((cache, node, index) => { const [ip, port] = node.trim().split(":"); if (ip && port) { - cache[`node-${index + 1}`] = { ip, id: parseInt(port, 10) }; + cache[`node-${index + 1}`] = { ip, port: parseInt(port, 10) }; } return cache; }, {} as NodeCache) @@ -260,10 +265,9 @@ async function ensureFileExists( await fs.promises.mkdir(dirPath, { recursive: true }); await fs.promises.writeFile(filePath, content, { flag: "w" }); logger.info(`File updated: ${filePath}`); - } catch (error) { - logger.error( - `Error creating/updating file ${filePath}: ${(error as Error).message}`, - ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } finally { await releaseLock(); } diff --git a/src/controllers/proxy.ts b/src/controllers/proxy.ts index 601f1556..c091590a 100644 --- a/src/controllers/proxy.ts +++ b/src/controllers/proxy.ts @@ -1,9 +1,9 @@ import { Application } from "express"; import logger from "../utils/logger"; -import { TRUSTED_PROXYS } from "../config/variables"; +import { TRUSTED_PROXIES } from "../config/variables"; export default function trustedProxies(app: Application) { - const trusted: string = TRUSTED_PROXYS; + const trusted: string = TRUSTED_PROXIES; if (!trusted) { logger.warn( diff --git a/src/controllers/scheduler.ts b/src/controllers/scheduler.ts index caa19481..db450d95 100644 --- a/src/controllers/scheduler.ts +++ b/src/controllers/scheduler.ts @@ -12,7 +12,8 @@ const scheduleFetch = () => { fetchData(); cleanupOldEntries(); } catch (error: unknown) { - logger.error(`Error during scheduled fetch: ${error}`); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } intervalId = setInterval(() => { @@ -81,8 +82,9 @@ const cleanupOldEntries = async () => { try { db.run("DELETE FROM data WHERE timestamp < ?", twentyFourHoursAgo, Error); logger.info("Old entries cleared from the database."); - } catch (Error: unknown) { - logger.error(`Error clearing old entries: ${(Error as Error).message}`); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } }; diff --git a/src/data/frontendConfiguration.json b/src/data/frontendConfiguration.json index fe51488c..0637a088 100644 --- a/src/data/frontendConfiguration.json +++ b/src/data/frontendConfiguration.json @@ -1 +1 @@ -[] +[] \ No newline at end of file diff --git a/src/handlers/api.ts b/src/handlers/api.ts index 6f62c056..fa7f1f7e 100644 --- a/src/handlers/api.ts +++ b/src/handlers/api.ts @@ -1,7 +1,7 @@ import extractRelevantData from "../utils/extractHostData"; import { Request, Response } from "express"; -import getDockerClient from "../utils/dockerClient"; -import fetchAllContainers from "../utils/containerService"; +import { getDockerClient } from "../utils/dockerClient"; +import { fetchAllContainers } from "../utils/containerService"; import { getCurrentSchedule } from "../controllers/scheduler"; import fs from "fs"; import checkReachability from "../utils/connectionChecker"; @@ -62,6 +62,10 @@ class ApiHandler { const version = await docker.version(); const relevantData = extractRelevantData({ hostName, info, version }); + if (!relevantData) { + ResponseHandler.error("No host found", 404); + } + return ResponseHandler.rawData(relevantData, "Fetched Host stats"); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); diff --git a/src/handlers/conf.ts b/src/handlers/conf.ts index e383c4d1..b49dd2a5 100644 --- a/src/handlers/conf.ts +++ b/src/handlers/conf.ts @@ -20,7 +20,7 @@ class ConfHandler { try { const { name, url, port } = req.query as unknown as target; if (!name || !url || !port) { - return ResponseHandler.denied("Name, Port, and URL are required."); + return ResponseHandler.error("Name, Port, and URL are required.", 400); } const config: dockerConfig = JSON.parse( @@ -28,7 +28,7 @@ class ConfHandler { ); if (config.hosts.some((host) => host.name === name)) { - return ResponseHandler.denied("Host already exists."); + return ResponseHandler.error("Host already exists.", 422); } config.hosts.push({ name, url, port }); @@ -47,7 +47,7 @@ class ConfHandler { const hostName = req.query.hostName as string; if (!hostName) { - return ResponseHandler.denied("Host name is required."); + return ResponseHandler.error("Host name is required.", 401); } const currentState = fs.readFileSync(configPath, "utf-8"); @@ -79,8 +79,9 @@ class ConfHandler { const newInterval = parseInterval(interval); if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { - return ResponseHandler.denied( + return ResponseHandler.error( "Interval must be between 5 minutes and 6 hours.", + 401, ); } diff --git a/src/handlers/data.ts b/src/handlers/data.ts index fd3515d6..5d3bf41c 100644 --- a/src/handlers/data.ts +++ b/src/handlers/data.ts @@ -2,6 +2,7 @@ import { Response, Request } from "express"; import db from "../config/db"; import { Table, DataRow } from "../typings/table"; import { createResponseHandler } from "./response"; +import logger from "../utils/logger"; function formatRows(rows: DataRow[]): Record { return rows.reduce( @@ -56,6 +57,35 @@ class DatabaseHandler { ); } + latestRaw(): Promise { + return new Promise((resolve, reject) => { + logger.debug("Reading DB"); + db.get( + "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", + (error: unknown, row: Partial> | undefined) => { + if (error) { + return reject(`Database query error: ${error}`); + } + + if (!row || !row.info) { + return reject("No data available for /data/latest"); + } + + try { + logger.info("Read latest data"); + const parsedData = JSON.parse(row.info); + logger.debug("Parsed data:", parsedData); + resolve(parsedData); + } catch (error: unknown) { + const errorMsg = + error instanceof Error ? error.message : String(error); + reject(`Error parsing data: ${errorMsg}`); + } + }, + ); + }); + } + all() { const ResponseHandler = createResponseHandler(this.res); const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); diff --git a/src/handlers/graph.ts b/src/handlers/graph.ts new file mode 100644 index 00000000..53e245f7 --- /dev/null +++ b/src/handlers/graph.ts @@ -0,0 +1,257 @@ +import cytoscape from "cytoscape"; +import logger from "../utils/logger"; +import { AllContainerData, ContainerData } from "./../typings/dockerConfig"; +import { atomicWrite } from "../utils/atomicWrite"; +import { rateLimitedReadFile } from "../utils/rateLimitFS"; + +const CACHE_DIR_JSON = "./src/data/graph.json"; +const CACHE_DIR_HTML = "./src/data/graph.html"; +const _assets = "./src/utils/assets"; +const serverSvg = `${_assets}/server-icon.svg`; +const containerSvg = `${_assets}/container-icon.svg`; +const pngPath = "./src/data/graph.png"; + +async function getPathData(path: string) { + try { + return await rateLimitedReadFile(path); + + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + return false; + } +} + +async function renderGraphToImage( + htmlContent: string, + outputImagePath: string, +): Promise { + let puppeteer; + try { + puppeteer = await import("puppeteer"); + } catch (error) { + logger.error("Puppeteer is not installed. Please install it to generate images."); + throw new Error(`Puppeteer is not installed (${error})`); + } + + let browser; + try { + browser = await puppeteer.default.launch({ + headless: "shell", + args: ["--disable-setuid-sandbox", "--no-sandbox"], + executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, + }); + + const page = await browser.newPage(); + await page.setContent(htmlContent, { waitUntil: "networkidle0" }); + await page.waitForSelector("#cy", { visible: true, timeout: 15000 }); + + await page.waitForFunction( + () => { + const cyElement = document.querySelector("#cy"); + return cyElement ? cyElement.children.length > 0 : false; + }, + { timeout: 10000 } + ); + + await page.screenshot({ + path: outputImagePath, + type: outputImagePath.endsWith(".jpg") ? "jpeg" : "png", + fullPage: true, + captureBeyondViewport: true, + }); + } catch (error: unknown) { + let errorMessage = "Unknown error occurred during browser operation"; + + if (error instanceof Error) { + errorMessage = error.message; + + // Detect common dependency errors + if (errorMessage.includes("libnss3") || errorMessage.includes("libxcb")) { + errorMessage = `❗ Missing system dependencies (libnss3)`; + } + + // Detect Chrome not found errors + if (errorMessage.includes("Failed to launch")) { + errorMessage = `❗ Chrome not found!`; + } + } + + throw new Error(`Graph rendering failed: ${errorMessage}`); + } finally { + if (browser) { + await browser.close().catch(() => { }); + } + } + + logger.info(`Graph rendered and image saved to: ${outputImagePath}`); +} + +async function generateGraphFiles( + allContainerData: AllContainerData, +): Promise { + if (process.env.CI === "true") { + logger.warn("Running inside a CI/CD Action, wont generated graphs"); + return false; + } else { + try { + logger.info("generateGraphFiles >>> Starting generation"); + const graphElements: cytoscape.ElementDefinition[] = []; + + for (const [hostName, containers] of Object.entries(allContainerData)) { + if ("error" in containers) { + // TODO: make error'ed hosts better + graphElements.push({ + data: { + id: hostName, + label: `Host: ${hostName} Error: ${containers.error}`, + type: "server", + }, + }); + } else { + const containerList = containers as ContainerData[]; + + // host node with container count + graphElements.push({ + data: { + id: hostName, + label: `${hostName} - ${containerList.length} Containers`, + type: "server", + }, + }); + + for (const container of containerList) { + // container node + graphElements.push({ + data: { + id: container.id, + label: `${container.name} (${container.state})`, + type: "container", + }, + }); + + // edge between host and container + graphElements.push({ + data: { + source: hostName, + target: container.id, + }, + }); + } + } + } + + atomicWrite(CACHE_DIR_JSON, JSON.stringify(graphElements, null, 2)); + + const htmlContent = ` + + + + + + Cytoscape Graph + + + + +
+ + + + `; + + atomicWrite(CACHE_DIR_HTML, htmlContent); + await renderGraphToImage(htmlContent, pngPath) + .then(() => logger.debug("HTML converted to image successfully!")) + .catch((err) => logger.error("Error:", err)); + + logger.info("generateGraphFiles <<< Files generated successfully"); + return true; + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + return false; + } + } +} + +function getGraphFilePaths() { + return { json: CACHE_DIR_JSON, html: CACHE_DIR_HTML }; +} + +export { generateGraphFiles, getGraphFilePaths }; diff --git a/src/handlers/notification.ts b/src/handlers/notification.ts index ad5c2938..9c10a599 100644 --- a/src/handlers/notification.ts +++ b/src/handlers/notification.ts @@ -27,7 +27,10 @@ class NotificationHandler { if (error) { return ResponseHandler.error(error as string, 400); } - return ResponseHandler.rawData(data, "Fetched notification template"); + return ResponseHandler.rawData( + JSON.parse(data), + "Fetched notification template", + ); }); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); diff --git a/src/handlers/response.ts b/src/handlers/response.ts index 8c6e95b8..ee062102 100644 --- a/src/handlers/response.ts +++ b/src/handlers/response.ts @@ -29,7 +29,7 @@ class ResponseHandler { } critical(log: string) { - logger.error(log); + logger.error(log.replace(/\n|\r/g, "")); this.res.status(500).json({ status: "critical", message: "Please see the server logs for more info", diff --git a/src/handlers/stack.ts b/src/handlers/stack.ts new file mode 100644 index 00000000..e87b533f --- /dev/null +++ b/src/handlers/stack.ts @@ -0,0 +1,162 @@ +import { Response, Request } from "express"; +import { + createStack, + getStackConfig, + getStackCompose, + writeEnvFile, + getEnvFile, +} from "../config/stacks"; +import { DockerComposeFile } from "../typings/dockerCompose"; +import logger from "../utils/logger"; +import * as compose from "docker-compose"; +import { createResponseHandler } from "./response"; +import { stackConfig } from "../typings/stackConfig"; +import { dockerStackEnv } from "../typings/dockerStackEnv"; +import path from "path"; + +const PROJECT_ROOT = path.resolve(__dirname, "../.."); + +export async function validate(name: string): Promise { + const config: stackConfig = JSON.parse(await getStackConfig()); + if (!config.stacks.find((element) => element === name)) { + throw new Error("Stack not found"); + } + + return true; +} + +async function composeAction(option: string, name: string): Promise { + const composeFile: string = path.join(PROJECT_ROOT, `stacks/${name}`); + switch (option) { + case "start": { + await compose.upAll({ cwd: composeFile, log: false }).then( + () => { + return true; + }, + (err: unknown) => { + throw new Error(err as string); + }, + ); + break; + } + case "stop": { + await compose.downAll({ cwd: composeFile, log: false }).then( + () => { + return true; + }, + (err: unknown) => { + throw new Error(err as string); + }, + ); + break; + } + } +} + +class StackHandler { + private req: Request; + private res: Response; + + constructor(req: Request, res: Response) { + this.req = req; + this.res = res; + } + + async createStack(req: Request, res: Response) { + const ResponseHandler = createResponseHandler(res); + try { + const name: string = req.params.name; + const content: DockerComposeFile = req.body; + let override = false; + override = req.query.override == "true"; + + await createStack(name, content, override); + return ResponseHandler.ok("Stack created"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async start(req: Request, res: Response) { + const ResponseHandler = createResponseHandler(res); + try { + const name: string = req.params.name; + await validate(name); + await composeAction("start", name); + return ResponseHandler.ok("Stack started"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async stop(req: Request, res: Response) { + const ResponseHandler = createResponseHandler(res); + try { + const name: string = req.params.name; + await validate(name); + await composeAction("stop", name); + return ResponseHandler.ok("Stack stopped"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } + } + + async stackCompose(req: Request, res: Response) { + const ResponseHandler = createResponseHandler(res); + try { + const {name} = req.params; + return ResponseHandler.rawData( + await getStackCompose(name), + "Stack compose fetched", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg.replace(/\n|\r/g, "")); + throw new Error(errorMsg); + } + } + + async setStackEnv(req: Request, res: Response) { + const ResponseHandler = createResponseHandler(res); + try { + const data: dockerStackEnv = req.body; + const name: string = req.params.name; + if (await writeEnvFile(name, data)) { + return ResponseHandler.ok("Wrote docker.env"); + } else { + return ResponseHandler.critical( + "Something went wrong while writing the env File!", + ); + } + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg.replace(/\n|\r/g, "")); + throw new Error(errorMsg); + } + } + + async getStackEnv(req: Request, res: Response) { + const ResponseHandler = createResponseHandler(res); + try { + const name: string = req.params.name; + const data = await getEnvFile(name); + if (data == null) { + return ResponseHandler.error( + "No environment file found for this Stack!", + 404, + ); + } + return ResponseHandler.rawData(data, "Read docker.env"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg.replace(/\n|\r/g, "")); + throw new Error(errorMsg); + } + } +} + +export const createStackHandler = (req: Request, res: Response) => + new StackHandler(req, res); diff --git a/src/init.ts b/src/init.ts index 8c757379..188542f6 100644 --- a/src/init.ts +++ b/src/init.ts @@ -7,27 +7,47 @@ import frontend from "./routes/frontendController/routes"; import api from "./routes/getter/routes"; import notificationService from "./routes/notifications/routes"; import conf from "./routes/setter/routes"; +import graph from "./routes/graphs/routes"; import authMiddleware from "./middleware/authMiddleware"; import ha from "./routes/highavailability/routes"; import trustedProxies from "./controllers/proxy"; import { limiter } from "./middleware/rateLimiter"; import { scheduleFetch } from "./controllers/scheduler"; +import { Server } from 'http'; import cors from "cors"; +import { setupWebSocket } from "./utils/webSocket"; +import stacks from "./routes/stack/routes"; import { blockWhileLocked } from "./middleware/checkLock"; import logger from "./utils/logger"; import initFiles from "./config/initFiles"; const LAB = [limiter, authMiddleware, blockWhileLocked]; -const initializeApp = (app: express.Application): void => { +const initializeApp = (app: express.Application, server: Server): void => { initFiles(); + + try { + logger.debug("Starting Websocket server, with these endpoints:"); + logger.debug("ws://localhost:9876/wss/container-data") + logger.debug("ws://localhost:9876/wss/server-logs") + setupWebSocket(server); + } catch (error: unknown) { + logger.error("Error starting WebSocket: ", error) + } + app.use(cors()); app.use(express.json()); - app.use("/api-docs", (req: Request, res: Response, next: NextFunction) => - next(), - ); - swaggerDocs(app); + if (process.env.NODE_ENV !== "production") { + app.use("/api-docs", (req: Request, res: Response, next: NextFunction) => + next(), + ); + app.get("/", (req: Request, res: Response) => { + res.redirect("/api-docs"); + }); + swaggerDocs(app); + } + trustedProxies(app); scheduleFetch(); @@ -36,13 +56,11 @@ const initializeApp = (app: express.Application): void => { app.use("/auth", LAB, auth); app.use("/data", LAB, data); app.use("/frontend", LAB, frontend); + app.use("/graph", LAB, graph); app.use("/notification-service", LAB, notificationService); + app.use("/stacks", LAB, stacks); app.use("/ha", limiter, authMiddleware, ha); - app.get("/", (req: Request, res: Response) => { - res.redirect("/api-docs"); - }); - process.on("exit", (code: number) => { logger.warn(`Server exiting (Code: ${code})`); }); diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts index 4afb3939..414b2762 100644 --- a/src/middleware/authMiddleware.ts +++ b/src/middleware/authMiddleware.ts @@ -36,7 +36,7 @@ async function authMiddleware( storedData.hash, ); if (!passwordMatch) { - ResponseHandler.denied("Invalid Password"); + ResponseHandler.error("Invalid Password", 402); return; } diff --git a/src/misc/createEnvDev.sh b/src/misc/createEnvDev.sh index 4a5a0bbe..1f231aa6 100755 --- a/src/misc/createEnvDev.sh +++ b/src/misc/createEnvDev.sh @@ -3,6 +3,9 @@ # Version VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" +# Automatic Stack environment management +AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT="${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT:-true}" + # Docker if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then RUNNING_IN_DOCKER="true" @@ -10,11 +13,14 @@ else RUNNING_IN_DOCKER="false" fi +# Default dev log level +LOG_LEVEL="${LOG_LEVEL:-debug}" + echo -n "\ { \"VERSION\": \"${VERSION}\", \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", - \"TRUSTED_PROXYS\": \"${TRUSTED_PROXYS}\", + \"TRUSTED_PROXIES\": \"${TRUSTED_PROXIES}\", \"HA_MASTER\": \"${HA_MASTER}\", \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", \"HA_NODE\": \"${HA_NODE}\", @@ -31,6 +37,8 @@ echo -n "\ \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", - \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\" + \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\", + \"AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT\": \"${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT}\", + \"LOG_LEVEL\": \"${LOG_LEVEL}\" } \ -" > ./src/data/variables.json +" > ./src/data/variables.json || exit 1 diff --git a/src/misc/createEnvFile.sh b/src/misc/createEnvFile.sh index 754eab5a..0fbd15de 100755 --- a/src/misc/createEnvFile.sh +++ b/src/misc/createEnvFile.sh @@ -3,6 +3,9 @@ # Version VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" +# Automatic Stack environment management +AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT="${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT:-true}" + # Docker if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then RUNNING_IN_DOCKER="true" @@ -10,11 +13,14 @@ else RUNNING_IN_DOCKER="false" fi +# Default log level +LOG_LEVEL="${LOG_LEVEL:-info}" + echo -n "\ { \"VERSION\": \"${VERSION}\", \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", - \"TRUSTED_PROXYS\": \"${TRUSTED_PROXYS}\", + \"TRUSTED_PROXIES\": \"${TRUSTED_PROXIES}\", \"HA_MASTER\": \"${HA_MASTER}\", \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", \"HA_NODE\": \"${HA_NODE}\", @@ -31,6 +37,8 @@ echo -n "\ \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", - \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\" + \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\", + \"AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT\": \"${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT}\", + \"LOG_LEVEL\": \"${LOG_LEVEL}\" } \ -" > /api/src/data/variables.json +" > /api/src/data/variables.json || exit 1 diff --git a/src/misc/dependencyGraphs/createDependencyGraph.sh b/src/misc/dependencyGraphs/createDependencyGraph.sh index 4e118194..5fe007aa 100755 --- a/src/misc/dependencyGraphs/createDependencyGraph.sh +++ b/src/misc/dependencyGraphs/createDependencyGraph.sh @@ -11,7 +11,7 @@ spawn_worker(){ echo -e "\nRoute: $route \n${target_route}" - npx depcruise \ + test=true depcruise \ -c ./src/misc/dependencyGraphs/.dependency-cruiser.cjs \ -p cli-feedback \ -T mermaid \ diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt index ad02a822..1cb2ebe8 100644 --- a/src/misc/dependencyGraphs/mermaid-all.txt +++ b/src/misc/dependencyGraphs/mermaid-all.txt @@ -2,106 +2,112 @@ flowchart TB subgraph 0["src"] 1["server.ts"] -subgraph 2["controllers"] -3["highAvailability.ts"] -C["proxy.ts"] -D["scheduler.ts"] -F["fetchData.ts"] -Q["auth.ts"] -X["frontendConfiguration.ts"] -end -subgraph 4["config"] -5["variables.ts"] -B["initFiles.ts"] -E["db.ts"] -end -subgraph 6["data"] -7["variables.json"] -end -subgraph 8["typings"] -9["ha.ts"] -end -A["init.ts"] -subgraph G["middleware"] -H["authMiddleware.ts"] -K["checkLock.ts"] -L["rateLimiter.ts"] -end -subgraph I["handlers"] -J["response.ts"] -P["auth.ts"] -T["data.ts"] -W["frontend.ts"] -10["api.ts"] +2["init.ts"] +subgraph 3["config"] +4["initFiles.ts"] +7["variables.ts"] +B["db.ts"] +end +subgraph 5["controllers"] +6["proxy.ts"] +A["scheduler.ts"] +C["fetchData.ts"] +N["auth.ts"] +U["frontendConfiguration.ts"] +14["highAvailability.ts"] +end +subgraph 8["data"] +9["variables.json"] +end +subgraph D["middleware"] +E["authMiddleware.ts"] +H["checkLock.ts"] +I["rateLimiter.ts"] +end +subgraph F["handlers"] +G["response.ts"] +M["auth.ts"] +Q["data.ts"] +T["frontend.ts"] +X["api.ts"] +10["graph.ts"] 13["ha.ts"] -16["notification.ts"] -19["conf.ts"] +19["notification.ts"] +1C["conf.ts"] end -subgraph M["routes"] -subgraph N["auth"] -O["routes.ts"] +subgraph J["routes"] +subgraph K["auth"] +L["routes.ts"] end -subgraph R["data"] +subgraph O["data"] +P["routes.ts"] +end +subgraph R["frontendController"] S["routes.ts"] end -subgraph U["frontendController"] -V["routes.ts"] +subgraph V["getter"] +W["routes.ts"] end -subgraph Y["getter"] +subgraph Y["graphs"] Z["routes.ts"] end subgraph 11["highavailability"] 12["routes.ts"] end -subgraph 14["notifications"] -15["routes.ts"] -end -subgraph 17["setter"] +subgraph 17["notifications"] 18["routes.ts"] end +subgraph 1A["setter"] +1B["routes.ts"] +end +end +subgraph 15["typings"] +16["ha.ts"] end end -1-->3 -1-->A -3-->5 -3-->9 -5-->7 +1-->2 +2-->4 +2-->6 +2-->A +2-->E +2-->H +2-->I +2-->L +2-->P +2-->S +2-->W +2-->Z +2-->12 +2-->18 +2-->1B +6-->7 +7-->9 A-->B A-->C -A-->D -A-->H -A-->K -A-->L -A-->O -A-->S -A-->V -A-->Z -A-->12 -A-->15 -A-->18 -C-->5 -D-->E -D-->F -F-->E -H-->J -K-->J -O-->P +C-->B +E-->G +H-->G +L-->M +M-->N +M-->G P-->Q -P-->J +Q-->B +Q-->G S-->T -T-->E -T-->J -V-->W +T-->U +T-->G W-->X -W-->J +X-->A +X-->G Z-->10 -10-->D -10-->J +Z-->G 12-->13 -13-->3 -13-->J -15-->16 -16-->J +13-->14 +13-->G +14-->7 +14-->16 18-->19 -19-->D -19-->J +19-->G +1B-->1C +1C-->A +1C-->G diff --git a/src/misc/dependencyGraphs/mermaid-graph.txt b/src/misc/dependencyGraphs/mermaid-graph.txt new file mode 100644 index 00000000..34484535 --- /dev/null +++ b/src/misc/dependencyGraphs/mermaid-graph.txt @@ -0,0 +1,15 @@ +flowchart TB + +subgraph 0["src"] +subgraph 1["routes"] +subgraph 2["graphs"] +3["routes.ts"] +end +end +subgraph 4["handlers"] +5["graph.ts"] +6["response.ts"] +end +end +3-->5 +3-->6 diff --git a/src/misc/entrypoint.sh b/src/misc/entrypoint.sh index 60b8a0e4..77b6236e 100755 --- a/src/misc/entrypoint.sh +++ b/src/misc/entrypoint.sh @@ -3,6 +3,12 @@ VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" +if [[ "$1" = "--dev" ]]; then + node_env="development" +elif [[ "$1" = "--prod" ]]; then + node_env="production" +fi + echo -e " \033[1;32mWelcome to\033[0m @@ -27,4 +33,4 @@ DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simpl bash ./createEnvFile.sh -exec node src/server.js +NODE_ENV=${node_env} node src/server.js diff --git a/src/misc/minifyDist.sh b/src/misc/minifyDist.sh index 8a85b162..171ef095 100755 --- a/src/misc/minifyDist.sh +++ b/src/misc/minifyDist.sh @@ -4,7 +4,7 @@ dist="$(pwd)/dist" run_script() { npx uglifyjs --no-annotations --in-situ "$1" > /dev/null - echo "✔️ Minified : $(basename "$1")" + echo "✔️ Minified : $(basename "$1")" } if [ -d "$dist" ]; then diff --git a/src/routes/auth/routes.ts b/src/routes/auth/routes.ts index 47ff6f25..03549bfa 100644 --- a/src/routes/auth/routes.ts +++ b/src/routes/auth/routes.ts @@ -3,54 +3,12 @@ import { createAuthenticationHandler } from "../../handlers/auth"; const router = Router(); -/** - * @swagger - * /auth/enable: - * post: - * summary: Enable authentication by setting a password - * tags: [Authentication] - * parameters: - * - name: password - * in: query - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Authentication enabled. - * 400: - * description: Password is required. - * 500: - * description: Error saving password. - */ router.post("/enable", async (req: Request, res: Response): Promise => { const password = req.query.password as string; const handler = createAuthenticationHandler(req, res); await handler.enable(password); }); -/** - * @swagger - * /auth/disable: - * post: - * summary: Disable authentication by providing the existing password - * tags: [Authentication] - * parameters: - * - name: password - * in: query - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Authentication disabled. - * 400: - * description: Password is required. - * 401: - * description: Invalid password. - * 500: - * description: Error disabling authentication. - */ router.post("/disable", async (req: Request, res: Response): Promise => { const password = req.query.password as string; const handler = createAuthenticationHandler(req, res); diff --git a/src/routes/data/routes.ts b/src/routes/data/routes.ts index 92a7f976..93c4610b 100644 --- a/src/routes/data/routes.ts +++ b/src/routes/data/routes.ts @@ -2,150 +2,16 @@ import express, { Request, Response } from "express"; const router = express.Router(); import { createDatabaseHandler } from "../../handlers/data"; -/** - * @swagger - * /data/latest: - * get: - * summary: Retrieve the latest container statistics for a specific host - * tags: [Database queries] - * responses: - * 200: - * description: A JSON object containing the latest container statistics for the specified host. - * content: - * application/json: - * schema: - * type: object - * properties: - * Fin-2: - * type: array - * items: - * type: object - * properties: - * name: - * type: string - * description: The name of the container - * example: "Container A" - * id: - * type: string - * description: Unique identifier for the container - * example: "abcd1234" - * hostName: - * type: string - * description: Name of the host system running this container - * example: "Fin-2" - * state: - * type: string - * description: Current state of the container - * example: "running" - * cpu_usage: - * type: number - * description: CPU usage percentage for this container - * example: 30 - * mem_usage: - * type: number - * description: Memory usage in bytes - * example: 2097152 - * mem_limit: - * type: number - * description: Memory limit in bytes set for this container - * example: 8123764736 - * net_rx: - * type: number - * description: Total network received bytes since container start - * example: 151763111 - * net_tx: - * type: number - * description: Total network transmitted bytes since container start - * example: 7104386 - * current_net_rx: - * type: number - * description: Current received bytes in the recent period - * example: 1048576 - * current_net_tx: - * type: number - * description: Current transmitted bytes in the recent period - * example: 524288 - * networkMode: - * type: string - * description: Networking mode for the container - * example: "bridge" - */ router.get("/latest", (req: Request, res: Response) => { const DatabaseHandler = createDatabaseHandler(req, res); return DatabaseHandler.latest(); }); -/** - * @swagger - * /data/all: - * get: - * summary: Retrieve container statistics entries from the last 24 hours - * tags: [Database queries] - * responses: - * 200: - * description: A numbered array of 'info' JSON objects from the last 24 hours. - * content: - * application/json: - * schema: - * type: object - * properties: - * 0: - * type: object - * description: Statistics for the first entry within 24 hours. - * properties: - * name: - * type: string - * example: "Container A" - * id: - * type: string - * example: "abcd1234" - * cpu_usage: - * type: number - * example: 30 - * mem_usage: - * type: number - * example: 2048 - * 1: - * type: object - * description: Statistics for the second entry within 24 hours. - * properties: - * name: - * type: string - * example: "Container B" - * id: - * type: string - * example: "efgh5678" - * cpu_usage: - * type: number - * example: 45 - * mem_usage: - * type: number - * example: 3072 - */ router.get("/all", (req: Request, res: Response) => { const DatabaseHandler = createDatabaseHandler(req, res); return DatabaseHandler.all(); }); -/** - * @swagger - * /data/clear: - * delete: - * summary: Clear all container statistics entries from the database - * tags: [Database queries] - * responses: - * 200: - * description: A message indicating whether the database was cleared successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * description: Success message upon database clearance - * example: "Database cleared successfully." - */ router.delete("/clear", (req: Request, res: Response) => { const DatabaseHandler = createDatabaseHandler(req, res); return DatabaseHandler.clear(); diff --git a/src/routes/frontendController/routes.ts b/src/routes/frontendController/routes.ts index 39500c51..723afa47 100644 --- a/src/routes/frontendController/routes.ts +++ b/src/routes/frontendController/routes.ts @@ -2,259 +2,30 @@ import express from "express"; const router = express.Router(); import { createFrontendHandler } from "../../handlers/frontend"; -/** - * @swagger - * /frontend/show/{containerName}: - * post: - * summary: Unhide a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to unhide - * responses: - * 200: - * description: Container unhidden successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.post("/show/:containerName", async (req, res) => { const FrontendHandler = createFrontendHandler(req, res); const containerName = req.params.containerName; return FrontendHandler.show(containerName); }); -/** - * @swagger - * /frontend/tag/{containerName}/{tag}: - * post: - * summary: Add a tag to a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to add tag to - * - in: path - * name: tag - * schema: - * type: string - * required: true - * description: The tag to add - * responses: - * 200: - * description: Tag added successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.post("/tag/:containerName/:tag", async (req, res) => { const { containerName, tag } = req.params; const FrontendHandler = createFrontendHandler(req, res); return FrontendHandler.addTag(containerName, tag); }); -/** - * @swagger - * /frontend/pin/{containerName}: - * post: - * summary: Pin a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to pin - * responses: - * 200: - * description: Container pinned successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.post("/pin/:containerName", async (req, res) => { const { containerName } = req.params; const FrontendHandler = createFrontendHandler(req, res); return FrontendHandler.pin(containerName); }); -/** - * @swagger - * /frontend/add-link/{containerName}/{link}: - * post: - * summary: Add a link to a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to add link to - * - in: path - * name: link - * schema: - * type: string - * required: true - * description: The link to add - * responses: - * 200: - * description: Link added successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.post("/add-link/:containerName/:link", async (req, res) => { const { containerName, link } = req.params; const FrontendHandler = createFrontendHandler(req, res); return FrontendHandler.addLink(containerName, link); }); -/** - * @swagger - * /frontend/add-icon/{containerName}/{icon}/{useCustomIcon}: - * post: - * summary: Add an Icon to a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to add link to - * - in: path - * name: icon - * schema: - * type: string - * required: true - * description: The Icon to add - * - in: path - * name: useCustomIcon - * shema: - * type: boolean - * required: false - * description: If this icon is a custom icon or nor - * responses: - * 200: - * description: Icon added successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.post( "/add-icon/:containerName/:icon/:useCustomIcon", async (req, res) => { @@ -272,242 +43,30 @@ router.post( |____/|_____|_____|_____| |_| |_____| */ -/** - * @swagger - * /frontend/hide/{containerName}: - * delete: - * summary: Hide a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to hide - * responses: - * 200: - * description: Container hidden successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ -// Hide a container router.delete("/hide/:containerName", async (req, res) => { const { containerName } = req.params; const FrontendHandler = createFrontendHandler(req, res); return FrontendHandler.hide(containerName); }); -/** - * @swagger - * /frontend/remove-tag/{containerName}/{tag}: - * delete: - * summary: Remove a tag from a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to remove tag from - * - in: path - * name: tag - * schema: - * type: string - * required: true - * description: The tag to remove - * responses: - * 200: - * description: Tag removed successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.delete("/remove-tag/:containerName/:tag", async (req, res) => { const { containerName, tag } = req.params; const FrontendHandler = createFrontendHandler(req, res); return FrontendHandler.removeTag(containerName, tag); }); -/** - * @swagger - * /frontend/unpin/{containerName}: - * delete: - * summary: Unpin a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to unpin - * responses: - * 200: - * description: Container unpinned successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.delete("/unpin/:containerName", async (req, res) => { const { containerName } = req.params; const FrontendHandler = createFrontendHandler(req, res); return FrontendHandler.unPin(containerName); }); -/** - * @swagger - * /frontend/remove-link/{containerName}: - * delete: - * summary: Remove a link from a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to remove link from - * responses: - * 200: - * description: Link removed successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.delete("/remove-link/:containerName", async (req, res) => { const { containerName } = req.params; const FrontendHandler = createFrontendHandler(req, res); return FrontendHandler.removeLink(containerName); }); -/** - * @swagger - * /frontend/remove-icon/{containerName}: - * delete: - * summary: Remove an icon from a container - * tags: [Frontend Configuration] - * parameters: - * - in: path - * name: containerName - * schema: - * type: string - * required: true - * description: The name of the container to remove the icon from - * responses: - * 200: - * description: Icon removed successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * error: - * type: string - * description: Error message - */ router.delete("/remove-icon/:containerName", async (req, res) => { const { containerName } = req.params; const FrontendHandler = createFrontendHandler(req, res); diff --git a/src/routes/getter/routes.ts b/src/routes/getter/routes.ts index 0912d48b..d08ae511 100644 --- a/src/routes/getter/routes.ts +++ b/src/routes/getter/routes.ts @@ -2,315 +2,42 @@ import { Router, Request, Response } from "express"; import { createApiHandler } from "../../handlers/api"; const router = Router(); -/** - * @swagger - * /api/hosts: - * get: - * summary: Retrieve a list of all available Docker hosts - * tags: [Hosts] - * responses: - * 200: - * description: A JSON object containing an array of host names. - * content: - * application/json: - * schema: - * type: object - * properties: - * hosts: - * type: array - * items: - * type: string - * example: ["local", "remote1"] - */ router.get("/hosts", (req: Request, res: Response) => { const ApiHandler = createApiHandler(req, res); return ApiHandler.hosts(); }); -/** - * @swagger - * /api/system: - * get: - * summary: Retrieve system configuration details - * tags: [Misc] - * responses: - * 200: - * description: A JSON object containing the system configuration details. - * content: - * application/json: - * schema: - * type: object - * description: The parsed configuration details. - * 500: - * description: An error occurred while fetching the system configuration. - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * description: Error message detailing the issue encountered. - */ router.get("/system", (req: Request, res: Response) => { const ApiHandler = createApiHandler(req, res); return ApiHandler.system(); }); -/** - * @swagger - * /api/host/{hostName}/stats: - * get: - * summary: Retrieve statistics for a specified Docker host - * tags: [Hosts] - * parameters: - * - name: hostName - * in: path - * required: true - * description: The name of the host for which to fetch statistics. - * schema: - * type: string - * responses: - * 200: - * description: A JSON object containing relevant statistics for the specified host. - * content: - * application/json: - * schema: - * type: object - * properties: - * hostName: - * type: string - * description: The name of the Docker host. - * info: - * type: object - * description: Information about the Docker host (e.g., storage, running containers). - * version: - * type: object - * description: Version details of the Docker installation on the host. - * 500: - * description: An error occurred while fetching host statistics. - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * description: Error message detailing the issue encountered. - */ router.get("/host/:hostName/stats", async (req: Request, res: Response) => { const { hostName } = req.params; const ApiHandler = createApiHandler(req, res); return ApiHandler.hostStats(hostName); }); -/** - * @swagger - * /api/containers: - * get: - * summary: Retrieve all Docker containers across all configured hosts - * tags: [Containers] - * responses: - * 200: - * description: A JSON object containing container data for all hosts. - * content: - * application/json: - * schema: - * type: object - * additionalProperties: - * type: object - * properties: - * name: - * type: string - * description: Name of the container. - * id: - * type: string - * description: Unique identifier for the container. - * hostName: - * type: string - * description: The host on which the container is running. - * state: - * type: string - * description: Current state of the container (e.g., running, exited). - * cpu_usage: - * type: number - * format: double - * description: CPU usage in nanoseconds. - * mem_usage: - * type: number - * description: Memory usage in bytes. - * mem_limit: - * type: number - * description: Memory limit in bytes. - * net_rx: - * type: number - * description: Total received bytes over the network. - * net_tx: - * type: number - * description: Total transmitted bytes over the network. - * current_net_rx: - * type: number - * description: Current received bytes over the network. - * current_net_tx: - * type: number - * description: Current transmitted bytes over the network. - * networkMode: - * type: string - * description: Network mode configured for the container. - * link: - * type: string - * description: Optional link to additional information. - * icon: - * type: string - * description: Optional icon representing the container. - * tags: - * type: string - * description: Optional tags associated with the container. - * 500: - * description: An error occurred while fetching container data. - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * description: Error message detailing the issue encountered. - */ router.get("/containers", async (req: Request, res: Response) => { const ApiHandler = createApiHandler(req, res); return ApiHandler.containers(); }); -/** - * @swagger - * /api/config: - * get: - * summary: Retrieve Docker configuration - * tags: [Configuration] - * responses: - * 200: - * description: A JSON object containing the Docker configuration. - * content: - * application/json: - * schema: - * type: object - * additionalProperties: true - * 500: - * description: An error occurred while loading the Docker configuration. - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * description: Error message detailing the issue encountered. - */ router.get("/config", async (req: Request, res: Response) => { const ApiHandler = createApiHandler(req, res); return ApiHandler.config(); }); -/** - * @swagger - * /api/current-schedule: - * get: - * summary: Get the current fetch schedule in seconds - * tags: [Configuration] - * responses: - * 200: - * description: Current fetch schedule retrieved successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * interval: - * type: integer - * description: Current fetch interval in seconds. - */ router.get("/current-schedule", (req: Request, res: Response) => { const ApiHandler = createApiHandler(req, res); return ApiHandler.currentSchedule(); }); -/** - * @swagger - * /api/status: - * get: - * summary: Check the DockStatAPI and docker socket status of each host - * tags: [Misc] - * description: Returns the status of the backend and online components, indicating which nodes are reachable or offline. - * responses: - * 200: - * description: Server and backend status - * content: - * application/json: - * schema: - * type: object - * properties: - * backendReachable: - * type: boolean - * example: true - * online: - * type: object - * properties: - * Host-1: - * type: boolean - * example: true - * Host-2: - * type: boolean - * example: false - */ - router.get("/status", async (req: Request, res: Response) => { const ApiHandler = createApiHandler(req, res); return ApiHandler.status(); }); -/** - * @swagger - * /api/frontend-config: - * get: - * summary: Get Frontend Configuration - * tags: [Configuration] - * description: Retrieves the frontend configuration data. - * responses: - * 200: - * description: Success - * content: - * application/json: - * schema: - * type: array - * items: - * type: object - * properties: - * name: - * type: string - * description: Container Name - * hidden: - * type: boolean - * description: Whether the container is hidden - * tags: - * type: array - * items: - * type: string - * description: Tags associated with the container - * pinned: - * type: boolean - * description: Whether the container is pinned - * 500: - * description: Internal Server Error - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - * description: Error message - */ router.get("/frontend-config", (req: Request, res: Response) => { const ApiHandler = createApiHandler(req, res); return ApiHandler.frontendConfig(); diff --git a/src/routes/graphs/routes.ts b/src/routes/graphs/routes.ts new file mode 100644 index 00000000..db532058 --- /dev/null +++ b/src/routes/graphs/routes.ts @@ -0,0 +1,31 @@ +import { Request, Response, Router } from "express"; +import { createResponseHandler } from "../../handlers/response"; +import path from "path"; +const router = Router(); + +router.get("/", async (req: Request, res: Response) => { + const ResponseHandler = createResponseHandler(res); + try { + const graphPath = path.join( + __dirname, + "/../../.." + "/src/data/graph.html", + ); + return res.contentType("html").status(200).sendFile(graphPath); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } +}); + +router.get("/image", async (req: Request, res: Response) => { + const ResponseHandler = createResponseHandler(res); + try { + const graphPath = path.join(__dirname, "/../../.." + "/src/data/graph.png"); + return res.contentType("image/png").status(200).sendFile(graphPath); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } +}); + +export default router; diff --git a/src/routes/highavailability/routes.ts b/src/routes/highavailability/routes.ts index 86057bcd..d4adc466 100644 --- a/src/routes/highavailability/routes.ts +++ b/src/routes/highavailability/routes.ts @@ -3,31 +3,11 @@ import { SyncRequestBody } from "../../typings/syncRequestBody"; import { createHaHandler } from "../../handlers/ha"; const router = Router(); -/** - * @swagger - * /ha/config: - * get: - * summary: Retrieve the High Availability Config - * tags: [High Availability] - * responses: - * 200: - * description: A JSON object containing the config. - */ router.get("/config", async (req: Request, res: Response) => { const HaHandler = createHaHandler(req, res); return HaHandler.config(); }); -/** - * @swagger - * /ha/sync: - * post: - * summary: Synchronize configuration files from master node. - * tags: [High Availability] - * responses: - * 200: - * description: Files synchronized successfully. - */ router.post( "/sync", async ( @@ -39,16 +19,6 @@ router.post( }, ); -/** - * @swagger - * /ha/prepare-sync: - * get: - * summary: Prepare files for synchronization. - * tags: [High Availability] - * responses: - * 200: - * description: A JSON object containing files to sync. - */ router.get("/prepare-sync", async (req: Request, res: Response) => { const HaHandler = createHaHandler(req, res); return HaHandler.prepare(); diff --git a/src/routes/notifications/routes.ts b/src/routes/notifications/routes.ts index 4544b8ce..13b754bd 100644 --- a/src/routes/notifications/routes.ts +++ b/src/routes/notifications/routes.ts @@ -2,125 +2,16 @@ import { Request, Response, Router } from "express"; import { createNotificationHandler } from "../../handlers/notification"; const router = Router(); -/** - * @swagger - * /notification-service/get-template: - * get: - * summary: Retrieve the notification template - * tags: [Notification Service] - * responses: - * 200: - * description: Template data retrieved successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful - * data: - * type: object - * description: The template data in JSON format - * 500: - * description: Internal server error. - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * description: Error message - */ router.get("/get-template", (req: Request, res: Response) => { const NotificationHandler = createNotificationHandler(req, res); return NotificationHandler.getTemplate(); }); -/** - * @swagger - * /notification-service/set-template: - * post: - * summary: Update the notification template - * tags: [Notification Service] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * description: New template data to save - * responses: - * 200: - * description: Template updated successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * description: Success message - * 500: - * description: Internal server error. - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * description: Error message - */ router.post("/set-template", (req: Request, res: Response): void => { const NotificationHandler = createNotificationHandler(req, res); return NotificationHandler.setTemplate(req); }); -/** - * @swagger - * /notification-service/test/{type}/{containerId}: - * post: - * summary: Send a test notification for a specific container - * tags: [Notification Service] - * parameters: - * - in: path - * name: type - * schema: - * type: string - * required: true - * description: Type of notification to test - * - in: path - * name: containerId - * schema: - * type: string - * required: true - * description: The ID of the container for the notification test - * responses: - * 200: - * description: Test notification sent successfully. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * 500: - * description: Internal server error. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - */ router.post("/test/:type/:containerId", async (req: Request, res: Response) => { const NotificationHandler = createNotificationHandler(req, res); NotificationHandler.test(req); diff --git a/src/routes/setter/routes.ts b/src/routes/setter/routes.ts index 75ef747d..16150293 100644 --- a/src/routes/setter/routes.ts +++ b/src/routes/setter/routes.ts @@ -1,83 +1,20 @@ import express, { Router, Request, Response } from "express"; -import { createConfHandler } from "../../handlers/conf"; const router: Router = express.Router(); +import { createConfHandler } from "../../handlers/conf"; -/** - * @swagger - * /conf/addHost: - * put: - * summary: Add a new host to the Docker configuration - * tags: [Configuration] - * parameters: - * - name: name - * in: query - * required: true - * description: The name of the new host. - * - name: url - * in: query - * required: true - * description: The URL of the new host. - * - name: port - * in: query - * required: true - * description: The port of the new host. - * responses: - * 200: - * description: Host added successfully. - * 400: - * description: Bad request, invalid input. - * 500: - * description: An error occurred while adding the host. - */ router.put("/addHost", async (req: Request, res: Response): Promise => { const ConfHandler = createConfHandler(req, res); return ConfHandler.addHost(req); }); -/** - * @swagger - * /conf/scheduler: - * put: - * summary: Set fetch interval for data fetching - * tags: [Configuration] - * parameters: - * - name: interval - * in: query - * required: true - * description: The new interval for fetching data, e.g., "6h 20m", "300s". - * responses: - * 200: - * description: Fetch interval set successfully. - * 400: - * description: Invalid interval format or out of range. - */ -router.put("/scheduler", (req: Request, res: Response) => { +router.delete("/removeHost", (req: Request, res: Response): void => { const ConfHandler = createConfHandler(req, res); - return ConfHandler.scheduler(req); + return ConfHandler.removeHost(req); }); -/** - * @swagger - * /conf/removeHost: - * delete: - * summary: Remove a host from the Docker configuration - * tags: [Configuration] - * parameters: - * - name: hostName - * in: query - * required: true - * description: The name of the host to remove. - * responses: - * 200: - * description: Host removed successfully. - * 404: - * description: Host not found. - * 500: - * description: An error occurred while removing the host. - */ -router.delete("/removeHost", (req: Request, res: Response): void => { +router.put("/scheduler", (req: Request, res: Response) => { const ConfHandler = createConfHandler(req, res); - return ConfHandler.addHost(req); + return ConfHandler.scheduler(req); }); export default router; diff --git a/src/routes/stack/routes.ts b/src/routes/stack/routes.ts new file mode 100644 index 00000000..8f9b9ae8 --- /dev/null +++ b/src/routes/stack/routes.ts @@ -0,0 +1,35 @@ +import express, { Router, Request, Response } from "express"; +const router: Router = express.Router(); +import { createStackHandler } from "../../handlers/stack"; + +router.post("/create/:name", async (req: Request, res: Response) => { + const StackHandler = createStackHandler(req, res); + return StackHandler.createStack(req, res); +}); + +router.post("/start/:name", async (req: Request, res: Response) => { + const StackHandler = createStackHandler(req, res); + return StackHandler.start(req, res); +}); + +router.post("/stop/:name", async (req: Request, res: Response) => { + const StackHandler = createStackHandler(req, res); + return StackHandler.stop(req, res); +}); + +router.get("/get/:name", async (req: Request, res: Response) => { + const StackHandler = createStackHandler(req, res); + return await StackHandler.stackCompose(req, res); +}); + +router.post("/set-env/:name", async (req: Request, res: Response) => { + const StackHandler = createStackHandler(req, res); + return await StackHandler.setStackEnv(req, res); +}); + +router.get("/get-env/:name", async (req: Request, res: Response) => { + const StackHandler = createStackHandler(req, res); + return await StackHandler.getStackEnv(req, res); +}); + +export default router; diff --git a/src/sample-variable.json b/src/sample-variable.json index 06153af5..f507796b 100644 --- a/src/sample-variable.json +++ b/src/sample-variable.json @@ -1,7 +1,7 @@ { "VERSION": "", "RUNNING_IN_DOCKER": "", - "TRUSTED_PROXYS": "", + "TRUSTED_PROXIES": "", "HA_MASTER": "", "HA_MASTER_IP": "", "HA_NODE": "", @@ -18,5 +18,7 @@ "TELEGRAM_BOT_TOKEN": "", "TELEGRAM_CHAT_ID": "", "WHATSAPP_API_URL": "", - "WHATSAPP_RECIPIENT": "" + "WHATSAPP_RECIPIENT": "", + "AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT": "true", + "LOG_LEVEL": "info" } diff --git a/src/server.ts b/src/server.ts index 97e5337a..edcb2ec5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,14 +1,18 @@ import express from "express"; import initializeApp from "./init"; -import { startMasterNode } from "./controllers/highAvailability"; import writeUserConf from "./config/hostsystem"; +import { startServer } from "./utils/startServer"; +import http from "http"; +const port: number = parseInt(process.env.PORT || "9876"); const app = express(); -const PORT: number = 9876; +const server = http.createServer(app); -writeUserConf(); -initializeApp(app); +initializeApp(app, server); -app.listen(PORT, () => { - startMasterNode(); -}); +if (process.env.NODE_ENV !== "testing") { + writeUserConf(port); + startServer(app, server, port); +} + +export default app; \ No newline at end of file diff --git a/src/typings/dockerCompose.ts b/src/typings/dockerCompose.ts new file mode 100644 index 00000000..e30f7e0d --- /dev/null +++ b/src/typings/dockerCompose.ts @@ -0,0 +1,92 @@ +export interface DockerComposeFile { + services: Record; + networks?: Record; + volumes?: Record; +} + +export interface ServiceDefinition { + image?: string; + build?: BuildDefinition; + container_name?: string; + command?: string | string[]; + environment?: Record; + ports?: string[] | PortMapping[]; + volumes?: string[]; + networks?: string[]; + restart?: string; + depends_on?: string[]; + deploy?: DeployDefinition; + env_file?: string[]; +} + +export interface BuildDefinition { + context: string; + dockerfile?: string; + args?: Record; + cache_from?: string[]; + labels?: Record; + target?: string; +} + +export interface PortMapping { + target: number; + published: number; + protocol?: "tcp" | "udp"; + mode?: "host" | "ingress"; +} + +export interface DeployDefinition { + replicas?: number; + resources?: ResourcesDefinition; + restart_policy?: RestartPolicyDefinition; + labels?: Record; + update_config?: UpdateConfigDefinition; +} + +export interface ResourcesDefinition { + limits?: ResourceLimits; + reservations?: ResourceReservations; +} + +export interface ResourceLimits { + cpus?: string; + memory?: string; +} + +export interface ResourceReservations { + cpus?: string; + memory?: string; +} + +export interface RestartPolicyDefinition { + condition?: "none" | "on-failure" | "any"; + delay?: string; + max_attempts?: number; + window?: string; +} + +export interface UpdateConfigDefinition { + parallelism?: number; + delay?: string; + failure_action?: "continue" | "pause"; + monitor?: string; + max_failure_ratio?: number; + order?: "start-first" | "stop-first"; +} + +export interface NetworkDefinition { + driver?: string; + driver_opts?: Record; + attachable?: boolean; + external?: boolean; + internal?: boolean; + labels?: Record; +} + +export interface VolumeDefinition { + driver?: string; + driver_opts?: Record; + external?: boolean; + labels?: Record; + name?: string; +} diff --git a/src/typings/dockerStackEnv.ts b/src/typings/dockerStackEnv.ts new file mode 100644 index 00000000..c784b85d --- /dev/null +++ b/src/typings/dockerStackEnv.ts @@ -0,0 +1,10 @@ +interface dockerStackProperty { + name: string; + value: string; +} + +interface dockerStackEnv { + environment: dockerStackProperty[]; +} + +export { dockerStackEnv, dockerStackProperty }; diff --git a/src/typings/ha.ts b/src/typings/ha.ts index a722fff8..f0352fc0 100644 --- a/src/typings/ha.ts +++ b/src/typings/ha.ts @@ -6,7 +6,7 @@ interface HighAvailabilityConfig { interface Node { ip: string; - id: number; + port: number; } interface HaNodeConfig { diff --git a/src/typings/stackConfig.ts b/src/typings/stackConfig.ts new file mode 100644 index 00000000..45c72553 --- /dev/null +++ b/src/typings/stackConfig.ts @@ -0,0 +1,5 @@ +interface stackConfig { + stacks: string[]; +} + +export { stackConfig }; diff --git a/src/utils/assets/api-icon.svg b/src/utils/assets/api-icon.svg new file mode 100644 index 00000000..5a4fdb7c --- /dev/null +++ b/src/utils/assets/api-icon.svg @@ -0,0 +1 @@ +\ diff --git a/src/utils/assets/container-icon.svg b/src/utils/assets/container-icon.svg new file mode 100644 index 00000000..15ed98c6 --- /dev/null +++ b/src/utils/assets/container-icon.svg @@ -0,0 +1 @@ +\ diff --git a/src/utils/assets/server-icon.svg b/src/utils/assets/server-icon.svg new file mode 100644 index 00000000..31c92d4a --- /dev/null +++ b/src/utils/assets/server-icon.svg @@ -0,0 +1 @@ +\ diff --git a/src/utils/atomicWrite.ts b/src/utils/atomicWrite.ts index 51f33759..d279475e 100644 --- a/src/utils/atomicWrite.ts +++ b/src/utils/atomicWrite.ts @@ -4,7 +4,7 @@ import { AtomicWriteOptions } from "../typings/atomicWrite"; export function atomicWrite( targetPath: string, - data: string | Buffer | Record, + data: object | string | Buffer | Record, options: AtomicWriteOptions = {}, ): void { const { mode = 0o600, exclusive = false } = options; diff --git a/src/utils/connectionChecker.ts b/src/utils/connectionChecker.ts index 85b00dde..5a45505b 100644 --- a/src/utils/connectionChecker.ts +++ b/src/utils/connectionChecker.ts @@ -59,8 +59,8 @@ async function checkReachability(): Promise { const hosts: target[] = parsedData.hosts; return await checkHostStatus(hosts); } catch (error: unknown) { - logger.error(`Error reading file: ${error as Error}`); - return undefined; + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } diff --git a/src/utils/containerService.ts b/src/utils/containerService.ts index f9277c1a..86dc2d38 100644 --- a/src/utils/containerService.ts +++ b/src/utils/containerService.ts @@ -1,16 +1,18 @@ import logger from "./logger"; -import { ContainerInfo, ContainerStats, ContainerInspectInfo } from "dockerode"; -import getDockerClient from "./dockerClient"; +import { ContainerInfo, } from "dockerode"; +import { getDockerClient } from "./dockerClient"; import fs from "fs"; import { atomicWrite } from "./atomicWrite"; const configPath = "./src/data/dockerConfig.json"; import { AllContainerData, HostConfig } from "../typings/dockerConfig"; +import { generateGraphFiles } from "../handlers/graph"; +import { WebSocket } from "ws"; -function loadConfig() { +export function loadConfig() { try { if (!fs.existsSync(configPath)) { logger.warn( - `Config file not found. Creating an empty file at ${configPath}`, + `Config file not found. Creating an empty file at ${configPath}` ); atomicWrite(configPath, JSON.stringify({ hosts: [] }, null, 2)); } @@ -19,96 +21,147 @@ function loadConfig() { logger.debug("Loaded " + configPath); return JSON.parse(configData); } catch (error: unknown) { - logger.error(`Failed to load config: ${(error as Error).message}`); - return null; + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + return { hosts: [] }; } } -async function fetchAllContainers(): Promise { +export async function fetchContainersForHost(hostName: string) { const config = loadConfig(); - if (!config || !config.hosts) { - logger.error("Invalid or missing host configuration."); - return {}; + const hostConfig = config.hosts.find((h: HostConfig) => h.name === hostName); + + if (!hostConfig) { + throw new Error(`Host ${hostName} not found in configuration`); } - const allContainerData: AllContainerData = {}; + try { + const docker = getDockerClient(hostName); + const containers: ContainerInfo[] = await docker.listContainers({ all: true }); - for (const hostConfig of config.hosts as HostConfig[]) { - const hostName = hostConfig.name; - try { - const docker = getDockerClient(hostName); - logger.debug(`Now processing: ${hostName}`); - const containers: ContainerInfo[] = await docker.listContainers({ - all: true, - }); - - allContainerData[hostName] = await Promise.all( - containers.map(async (container) => { - try { - const containerInstance = docker.getContainer(container.Id); - const containerInfo: ContainerInspectInfo = - await containerInstance.inspect(); - const containerStats: ContainerStats = - await containerInstance.stats({ stream: false }); - - const cpuDelta = - containerStats.cpu_stats.cpu_usage.total_usage - - containerStats.precpu_stats.cpu_usage.total_usage; - const systemCpuDelta = - containerStats.cpu_stats.system_cpu_usage - - containerStats.precpu_stats.system_cpu_usage; - const cpuUsage = - systemCpuDelta > 0 - ? (cpuDelta / systemCpuDelta) * - containerStats.cpu_stats.online_cpus - : 0; - - return { - name: container.Names[0].replace("/", ""), - id: container.Id, - hostName, - state: container.State, - cpu_usage: cpuUsage * 1000000000, - mem_usage: containerStats.memory_stats.usage, - mem_limit: containerStats.memory_stats.limit, - net_rx: containerStats.networks?.eth0?.rx_bytes || 0, - net_tx: containerStats.networks?.eth0?.tx_bytes || 0, - current_net_rx: containerStats.networks?.eth0?.rx_bytes || 0, - current_net_tx: containerStats.networks?.eth0?.tx_bytes || 0, - networkMode: containerInfo.HostConfig.NetworkMode || "unknown", - }; - } catch (containerError: unknown) { - logger.error( - `Error fetching details for container ID: ${container.Id} on host: ${hostName} - ${(containerError as Error).message}`, - ); - return { - name: container.Names[0].replace("/", ""), - id: container.Id, - hostName, - state: container.State, - cpu_usage: 0, - mem_usage: 0, - mem_limit: 0, - net_rx: 0, - net_tx: 0, - current_net_rx: 0, - current_net_tx: 0, - networkMode: "unknown", - }; - } - }), - ); - } catch (error: unknown) { - logger.error( - `Error fetching containers for host: ${hostName} - ${(error as Error).message}. Stack: ${(error as Error).stack}`, - ); - allContainerData[hostName] = { - error: `Error fetching containers: ${(error as Error).message}`, - }; - } + return await Promise.all( + containers.map(async (container) => { + try { + const containerInstance = docker.getContainer(container.Id); + const [containerInfo, containerStats] = await Promise.all([ + containerInstance.inspect(), + containerInstance.stats({ stream: false }), + ]); + + const cpuDelta = + containerStats.cpu_stats.cpu_usage.total_usage - + containerStats.precpu_stats.cpu_usage.total_usage; + const systemCpuDelta = + containerStats.cpu_stats.system_cpu_usage - + containerStats.precpu_stats.system_cpu_usage; + const cpuUsage = + systemCpuDelta > 0 + ? (cpuDelta / systemCpuDelta) * containerStats.cpu_stats.online_cpus + : 0; + + return { + name: container.Names[0].replace("/", ""), + id: container.Id, + hostName, + state: container.State, + cpu_usage: cpuUsage, + mem_usage: containerStats.memory_stats.usage, + mem_limit: containerStats.memory_stats.limit, + net_rx: containerStats.networks?.eth0?.rx_bytes || 0, + net_tx: containerStats.networks?.eth0?.tx_bytes || 0, + current_net_rx: containerStats.networks?.eth0?.rx_bytes || 0, + current_net_tx: containerStats.networks?.eth0?.tx_bytes || 0, + networkMode: containerInfo.HostConfig.NetworkMode || "unknown", + }; + } catch (error) { + logger.error(`Error processing container ${container.Id}: ${error}`); + return { + name: container.Names[0].replace("/", ""), + id: container.Id, + hostName, + state: container.State, + cpu_usage: 0, + mem_usage: 0, + mem_limit: 0, + net_rx: 0, + net_tx: 0, + current_net_rx: 0, + current_net_tx: 0, + networkMode: "unknown", + }; + } + }) + ); + } catch (error) { + logger.error(`Error fetching containers for ${hostName}: ${error}`); + throw error; } +} + +export async function fetchAllContainers(): Promise { + const config = loadConfig(); + const allContainerData: AllContainerData = {}; + await Promise.all( + config.hosts.map(async (hostConfig: HostConfig) => { + try { + allContainerData[hostConfig.name] = await fetchContainersForHost(hostConfig.name); + } catch (error) { + allContainerData[hostConfig.name] = { + error: `Error fetching containers: ${error instanceof Error ? error.message : String(error)}` + }; + } + }) + ); + + generateGraphFiles(allContainerData); return allContainerData; } -export default fetchAllContainers; +export async function streamContainerData(ws: WebSocket, hostName: string) { + try { + const containers = await fetchContainersForHost(hostName); + ws.send(JSON.stringify({ type: "containers", data: containers })); + + const docker = getDockerClient(hostName); + const eventStream = await docker.getEvents(); + + // eslint-disable-next-line + if (!(eventStream instanceof require('stream').Readable)) { + throw new Error('Failed to get valid event stream'); + } + + const handleData = (chunk: Buffer) => { + ws.send(JSON.stringify({ type: "container-event", data: chunk.toString() })); + }; + + const handleError = (err: Error) => { + logger.error(`Event stream error for ${hostName}: ${err.message}`); + ws.close(); + }; + + eventStream + .on('data', handleData) + .on('error', handleError); + + const closeHandler = () => { + eventStream + .removeListener('data', handleData) + .removeListener('error', handleError) + .removeListener('closed', handleError); + logger.info(`Closed event stream for ${hostName}`); + }; + + ws.on('close', closeHandler); + ws.on('error', closeHandler); + + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.error("Container data error:", message); + ws.send(JSON.stringify({ + error: "Failed to fetch container data", + details: message + })); + ws.close(); + } +} \ No newline at end of file diff --git a/src/utils/dockerClient.ts b/src/utils/dockerClient.ts index 8f2718b2..469c4096 100644 --- a/src/utils/dockerClient.ts +++ b/src/utils/dockerClient.ts @@ -1,7 +1,6 @@ import Docker from "dockerode"; import fs from "fs"; import logger from "./logger"; - import { dockerConfig, target } from "../typings/dockerConfig"; function loadDockerConfig(): dockerConfig { @@ -11,16 +10,15 @@ function loadDockerConfig(): dockerConfig { logger.debug("Refreshed DockerConfig.json"); return JSON.parse(rawData) as dockerConfig; } catch (error: unknown) { - logger.error( - "Error loading dockerConfig.json: " + (error as Error).message, - ); - throw new Error("Failed to load Docker configuration"); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); } } function createDockerClient(hostConfig: target): Docker { logger.info( - `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port || 2375}`, + `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port || 2375}` ); return new Docker({ host: hostConfig.url, @@ -29,7 +27,7 @@ function createDockerClient(hostConfig: target): Docker { }); } -const getDockerClient = (hostName: string): Docker => { +export const getDockerClient = (hostName: string): Docker => { logger.debug(`Getting Docker Client for ${hostName}`); const config = loadDockerConfig(); const hostConfig = config.hosts.find((host) => host.name === hostName); @@ -41,5 +39,3 @@ const getDockerClient = (hostName: string): Docker => { } return createDockerClient(hostConfig); }; - -export default getDockerClient; diff --git a/src/utils/extractHostData.ts b/src/utils/extractHostData.ts index 0af612ec..a383dc00 100644 --- a/src/utils/extractHostData.ts +++ b/src/utils/extractHostData.ts @@ -1,9 +1,54 @@ import { JsonData } from "../typings/hostData"; +import logger from "./logger"; type ComponentMap = Record; -// Export the function with type annotations -function extractRelevantData(jsonData: JsonData) { +interface RelevantData { + hostName: string; + info: { + ID: string; + Containers: number; + ContainersRunning: number; + ContainersPaused: number; + ContainersStopped: number; + Images: number; + OperatingSystem: string; + KernelVersion: string; + Architecture: string; + MemTotal: number; + NCPU: number; + }; + version: { + Components: ComponentMap; + }; +} + +function processComponents(components: unknown): ComponentMap { + try { + if (!Array.isArray(components)) return {}; + + return components.reduce((acc, component) => { + if ( + typeof component === 'object' && + component !== null && + 'Name' in component && + 'Version' in component + ) { + const { Name, Version } = component; + if (typeof Name === 'string' && typeof Version === 'string') { + acc[Name] = Version; + } + } + return acc; + }, {}); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`Error processing components: ${errorMessage}`); + return {}; + } +} + +export function extractRelevantData(jsonData: JsonData): RelevantData { return { hostName: jsonData.hostName, info: { @@ -20,29 +65,7 @@ function extractRelevantData(jsonData: JsonData) { NCPU: jsonData.info.NCPU, }, version: { - Components: (() => { - try { - if (!Array.isArray(jsonData?.version?.Components)) { - return {}; - } - - return jsonData.version.Components.reduce( - (acc, component) => { - if ( - typeof component?.Name === "string" && - typeof component?.Version === "string" - ) { - acc[component.Name] = component.Version; - } - return acc; - }, - {}, - ); - } catch (error) { - console.error("Error processing Components data:", error); - return {}; - } - })(), + Components: processComponents(jsonData?.version?.Components), }, }; } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 00adbdfc..2fd67bd5 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,7 +1,7 @@ import { createLogger, format, transports } from "winston"; import DailyRotateFile from "winston-daily-rotate-file"; +import { LOG_LEVEL } from "../config/variables"; -// ANSI color codes for log level customization const colors = { gray: "\x1b[90m", reset: "\x1b[0m", @@ -12,7 +12,6 @@ const colors = { blue: "\x1b[34m", }; -// Custom formatter to colorize log levels function colorizeLogLevel(level: string, levelName: string) { switch (level) { case "info": @@ -28,7 +27,7 @@ function colorizeLogLevel(level: string, levelName: string) { } } -// Filter out unwanted logs (example: Exit listeners logs) +// Filter out Exit listeners logs const filterLogs = format((info) => { if ( typeof info.message === "string" && @@ -39,9 +38,8 @@ const filterLogs = format((info) => { return info; }); -// Logger instance const logger = createLogger({ - level: "debug", + level: LOG_LEVEL, format: format.combine( filterLogs(), format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), @@ -56,7 +54,7 @@ const logger = createLogger({ info.level.toLowerCase(), level, ); - const message = `${colors.white}${info.message}${colors.reset}`; + const message = `${colors.white}${(info.message as string).replace(/\n|\r/g, "")}${colors.reset}`; return `${timestamp} ${levelColorized} : ${message}`; }), diff --git a/src/utils/notifications/_template.ts b/src/utils/notifications/_template.ts index 250f0950..fd5d71ed 100644 --- a/src/utils/notifications/_template.ts +++ b/src/utils/notifications/_template.ts @@ -14,7 +14,8 @@ function getTemplate(): Template | null { const data = fs.readFileSync(templatePath, "utf8"); return JSON.parse(data); } catch (error: unknown) { - logger.error("Failed to load template:", error as Error); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); return null; } } @@ -28,7 +29,8 @@ function setTemplate(newTemplate: string): void { ); logger.debug("Template updated successfully"); } catch (error: unknown) { - logger.error("Failed to update template:", error as Error); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } @@ -65,7 +67,8 @@ function renderTemplate(containerId: string): string | null { return text.replace(new RegExp(`{{${key}}}`, "g"), String(value)); }, template.text); } catch (error: unknown) { - logger.error("Failed to load containers:", error as Error); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); return null; } } diff --git a/src/utils/notifications/email.ts b/src/utils/notifications/email.ts index 4cd41a10..62b37d3a 100644 --- a/src/utils/notifications/email.ts +++ b/src/utils/notifications/email.ts @@ -47,6 +47,7 @@ export async function emailNotification(containerId: string) { try { await transporter.sendMail(mailOptions); } catch (error: unknown) { - logger.error("Error sending email:", error as Error); + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); } } diff --git a/src/utils/startServer.ts b/src/utils/startServer.ts new file mode 100644 index 00000000..7ca612f8 --- /dev/null +++ b/src/utils/startServer.ts @@ -0,0 +1,18 @@ +import { Express } from "express"; +import { Server } from 'http'; +import { startMasterNode } from "../controllers/highAvailability"; +import writeUserConf from "../config/hostsystem"; +import initFiles from "../config/initFiles"; + + +export function startServer(app: Express, server: Server, port: number) { + if (process.env.NODE_ENV === "testing") { + writeUserConf(port); + initFiles(); + } + + + server.listen(port, () => { + startMasterNode(); + }); +} \ No newline at end of file diff --git a/src/utils/swaggerDocs.ts b/src/utils/swaggerDocs.ts index 540304a7..7ed90d9d 100644 --- a/src/utils/swaggerDocs.ts +++ b/src/utils/swaggerDocs.ts @@ -1,11 +1,12 @@ import swaggerUi from "swagger-ui-express"; -import swaggerJsdoc from "swagger-jsdoc"; -import swaggerConfig from "../config/swaggerConfig"; +import { options } from "../config/swaggerConfig"; +import yaml from "yamljs"; import express from "express"; +import { SwaggerDefinition } from "swagger-jsdoc"; const swaggerDocs = (app: express.Application) => { - const specs = swaggerJsdoc(swaggerConfig); - app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs)); + const swaggerYaml: SwaggerDefinition = yaml.load("./src/config/swagger.yaml"); + app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerYaml, options)); }; export default swaggerDocs; diff --git a/src/utils/webSocket.ts b/src/utils/webSocket.ts new file mode 100644 index 00000000..893647e3 --- /dev/null +++ b/src/utils/webSocket.ts @@ -0,0 +1,113 @@ +import { Server } from 'http'; +import { WebSocketServer, WebSocket } from 'ws'; +import { URL } from 'url'; +import fs from 'fs'; +import logger from "./logger"; +import { streamContainerData } from './containerService'; + +export function setupWebSocket(server: Server) { + const wss = new WebSocketServer({ noServer: true }); + + server.on('upgrade', (req, socket, head) => { + logger.debug(`Received upgrade request for URL: ${req.url}`); + const baseURL = `http://${req.headers.host}/`; + const requestURL = new URL(req.url || '', baseURL); + const {pathname} = requestURL; + logger.debug(`Parsed pathname: ${pathname}`); + + // Debug log to verify path handling + logger.debug(`Handling upgrade for path: ${pathname}`); + + if (pathname === '/wss/container-data' || pathname === '/wss/server-logs') { + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit('connection', ws, req); + }); + } else { + logger.warn(`Rejected WebSocket connection to invalid path: ${pathname}`); + socket.write('HTTP/1.1 404 Not Found\r\n\r\n'); + socket.destroy(); + } + }); + + server.on("error", (error) => { + logger.error("HTTP server error:", error); + }); + + logger.debug("WebSocket server attached to HTTP server"); + + wss.on('connection', (ws: WebSocket, req) => { + const baseURL = `http://${req.headers.host}/`; + const requestURL = new URL(req.url || '', baseURL); + const {pathname} = requestURL; + + logger.info(`WebSocket connection established to ${pathname}`); + + const handleError = (error: string) => { + ws.send(JSON.stringify({ error })); + ws.close(); + }; + + if (pathname === '/wss/container-data') { + const hostName = requestURL.searchParams.get('host'); + if (!hostName) { + handleError('Missing required host parameter'); + return; + } + streamContainerData(ws, hostName); + } else if (pathname === '/wss/server-logs') { + const logFiles = fs.readdirSync("logs/").filter(file => file.startsWith('app-')); + + if (logFiles.length === 0) { + console.error('No log files found'); + return; + } + + const sortedLogFiles = logFiles.sort((a, b) => { + const dateA = a.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; + const dateB = b.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; + + return dateB.localeCompare(dateA); + }); + + const logPath = "logs/" + sortedLogFiles[0]; + + if (!fs.existsSync(logPath)) { + handleError('Log file not found'); + logger.error(`Log file ${logPath} not found`) + return; + } + + // Read the initial content of the log file + let lastSize = fs.statSync(logPath).size; + const history = fs.readFileSync(logPath, 'utf-8'); + ws.send(JSON.stringify({ type: 'log-history', data: history })); + + // Watch the log file for changes + const watcher = fs.watch(logPath, (eventType) => { + if (eventType === 'change') { + const newSize = fs.statSync(logPath).size; + if (newSize > lastSize) { + const stream = fs.createReadStream(logPath, { + start: lastSize, + end: newSize - 1, + encoding: 'utf-8' + }); + + stream.on('data', (chunk) => { + ws.send(JSON.stringify({ type: 'log-update', data: chunk })); + }); + + lastSize = newSize; + } + } + }); + + ws.on('close', () => { + watcher.close(); + logger.info('Closed WebSocket connection for logs'); + }); + } else { + handleError('Invalid WebSocket endpoint'); + } + }); +} \ No newline at end of file diff --git a/tests/main.spec.ts b/tests/main.spec.ts deleted file mode 100644 index f9006426..00000000 --- a/tests/main.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { test, expect } from '@playwright/test'; -import ora from 'ora'; - -interface Route { - url: string; -} - -interface FrontendRoute { - url: string; - type: string; -} - -const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - -test('Swagger - Auth enable and disable', async ({ page }) => { - await page.goto('http://localhost:9876/api-docs/'); - await page.getByLabel('post /auth/enable').click(); - await page.getByRole('button', { name: 'Try it out' }).click(); - await page.getByPlaceholder('password').click(); - await page.getByPlaceholder('password').fill('1'); - await page.getByRole('button', { name: 'Execute' }).click(); - await page.getByRole('button', { name: 'Authorize' }).click(); - await page.getByLabel('Value:').click(); - await page.getByLabel('Value:').fill('1'); - await page.getByLabel('Apply credentials').click(); - await page.getByRole('button', { name: 'Close' }).click(); - await page.getByLabel('post /auth/disable').click(); - await page.getByRole('button', { name: 'Try it out' }).click(); - await page.getByRole('row', { name: 'password *required (query)', exact: true }).getByPlaceholder('password').click(); - await page.getByRole('row', { name: 'password *required (query)', exact: true }).getByPlaceholder('password').fill('1'); - await page.locator('#operations-Authentication-post_auth_disable').getByRole('button', { name: 'Execute' }).click(); -}); - -test('Return 200 status code', async ({ request }) => { - await sleep(5000); - const getRoutes: Route[] = [ - { url: 'http://localhost:9876/data/latest' }, - { url: 'http://localhost:9876/data/time/24h' }, - { url: 'http://localhost:9876/api/hosts' }, - { url: 'http://localhost:9876/api/host/Fin-2/stats' }, - { url: 'http://localhost:9876/api/containers' }, - { url: 'http://localhost:9876/api/config' }, - { url: 'http://localhost:9876/api/current-schedule' }, - { url: 'http://localhost:9876/api/frontend-config' }, - { url: 'http://localhost:9876/api/status' }, - { url: 'http://localhost:9876/ha/config' }, - { url: 'http://localhost:9876/ha/prepare-sync' }, - { url: 'http://localhost:9876/notification-service/get-template' } - ]; - - for (const { url } of getRoutes) { - const spinner = ora(`Checking: ${url}`).start(); - const response = await request.get(`${url}`); - await sleep(1000); - if (response.status() === 200) { - spinner.succeed(`Checked: ${url}`); - } else { - spinner.fail(`Failed: ${url}`); - } - expect(response.status()).toBe(200); - } - - const putRoutes: Route[] = [ - { url: 'http://localhost:9876/conf/addHost?name=test&url=localhost&port=2375' }, - { url: 'http://localhost:9876/conf/scheduler?interval=300s' } - ]; - - for (const { url } of putRoutes) { - const spinner = ora(`Checking: ${url}`).start(); - const response = await request.put(`${url}`); - await sleep(1000); - if (response.status() === 200) { - spinner.succeed(`Checked: ${url}`); - } else { - spinner.fail(`Failed: ${url}`); - } - expect(response.status()).toBe(200); - } - - const data = { text: "{{name}} ({{id}}) on {{hostName}} is {{state}}." }; - - const spinner = ora('Checking: http://localhost:9876/notification-service/set-template').start(); - const response = await request.post('http://localhost:9876/notification-service/set-template', { data }); - await sleep(1000); - if (response.status() === 200) { - spinner.succeed('Checked: http://localhost:9876/notification-service/set-template'); - } else { - spinner.fail('Failed: http://localhost:9876/notification-service/set-template'); - } - expect(response.status()).toBe(200); - - // Remove test host: - const deleteSpinner = ora('Removing test host').start(); - await request.delete('http://localhost:9876/conf/removeHost?hostName=test'); - await sleep(1000); - deleteSpinner.succeed('Removed test host'); - - const frontendRoutes: FrontendRoute[] = [ - { url: 'http://localhost:9876/frontend/tag/test/test', type: "post" }, - { url: 'http://localhost:9876/frontend/pin/test', type: "post" }, - { url: 'http://localhost:9876/frontend/add-link/test/https%3A%2F%2Fexample.com', type: "post" }, - { url: 'http://localhost:9876/frontend/add-icon/test/test.png/true', type: "post" }, - { url: 'http://localhost:9876/frontend/hide/test', type: "delete" }, - { url: 'http://localhost:9876/frontend/remove-tag/test/test', type: "delete" }, - { url: 'http://localhost:9876/frontend/remove-link/test', type: "delete" }, - { url: 'http://localhost:9876/frontend/show/test', type: "post" }, - { url: 'http://localhost:9876/frontend/remove-icon/test', type: "delete" }, - { url: 'http://localhost:9876/frontend/unpin/test', type: "delete" } - ]; - - for (const { url, type } of frontendRoutes) { - const spinner = ora(`Checking: ${url}`).start(); - let response; - if (type === "post") { - response = await request.post(`${url}`); - } else if (type === "put") { - response = await request.put(`${url}`); - } else if (type === "delete") { - response = await request.delete(`${url}`); - } else { - throw new Error(`Unsupported request type: ${type}`); - } - await sleep(1000); - if (response.status() === 200) { - spinner.succeed(`Checked: ${url}`); - } else { - spinner.fail(`Failed: ${url}`); - } - expect(response.status()).toBe(200); - } -}); diff --git a/tsconfig.json b/tsconfig.json index 4af6b1d1..c4f6f4c0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,6 @@ }, "$schema": "https://json.schemastore.org/tsconfig", "display": "Recommended", - "include": ["src/**/*"], + "include": ["src/**/*", "**/*.d.ts", "__tests__/**/*"], "exclude": ["node_modules", "**/*.spec.ts"] } From ad79836fd0bb0a6047a71fd3d613fb8378bec997 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 9 Feb 2025 13:39:26 +0000 Subject: [PATCH 117/369] Automatically added GitHub issue links to TODOs --- src/handlers/graph.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/handlers/graph.ts b/src/handlers/graph.ts index 53e245f7..bf33d222 100644 --- a/src/handlers/graph.ts +++ b/src/handlers/graph.ts @@ -101,6 +101,7 @@ async function generateGraphFiles( for (const [hostName, containers] of Object.entries(allContainerData)) { if ("error" in containers) { // TODO: make error'ed hosts better + // Issue URL: https://github.com/Its4Nik/DockStatAPI/issues/32 graphElements.push({ data: { id: hostName, From 1aa44c6efda580d02004314b48e6abb417f95075 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 9 Feb 2025 20:06:46 +0100 Subject: [PATCH 118/369] Fix: Websocket logic adjustments --- src/handlers/graph.ts | 4 ++-- src/utils/webSocket.ts | 33 ++++++++++++++------------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/handlers/graph.ts b/src/handlers/graph.ts index bf33d222..6212adf5 100644 --- a/src/handlers/graph.ts +++ b/src/handlers/graph.ts @@ -77,14 +77,14 @@ async function renderGraphToImage( } } - throw new Error(`Graph rendering failed: ${errorMessage}`); + throw new Error(`Graph rendering failed - ${errorMessage}`); } finally { if (browser) { await browser.close().catch(() => { }); } } - logger.info(`Graph rendered and image saved to: ${outputImagePath}`); + logger.info(`Graph rendered and image saved to - ${outputImagePath}`); } async function generateGraphFiles( diff --git a/src/utils/webSocket.ts b/src/utils/webSocket.ts index 893647e3..0d412297 100644 --- a/src/utils/webSocket.ts +++ b/src/utils/webSocket.ts @@ -12,7 +12,7 @@ export function setupWebSocket(server: Server) { logger.debug(`Received upgrade request for URL: ${req.url}`); const baseURL = `http://${req.headers.host}/`; const requestURL = new URL(req.url || '', baseURL); - const {pathname} = requestURL; + const { pathname } = requestURL; logger.debug(`Parsed pathname: ${pathname}`); // Debug log to verify path handling @@ -38,7 +38,7 @@ export function setupWebSocket(server: Server) { wss.on('connection', (ws: WebSocket, req) => { const baseURL = `http://${req.headers.host}/`; const requestURL = new URL(req.url || '', baseURL); - const {pathname} = requestURL; + const { pathname } = requestURL; logger.info(`WebSocket connection established to ${pathname}`); @@ -83,27 +83,22 @@ export function setupWebSocket(server: Server) { ws.send(JSON.stringify({ type: 'log-history', data: history })); // Watch the log file for changes - const watcher = fs.watch(logPath, (eventType) => { - if (eventType === 'change') { - const newSize = fs.statSync(logPath).size; - if (newSize > lastSize) { - const stream = fs.createReadStream(logPath, { - start: lastSize, - end: newSize - 1, - encoding: 'utf-8' - }); - - stream.on('data', (chunk) => { - ws.send(JSON.stringify({ type: 'log-update', data: chunk })); - }); - - lastSize = newSize; - } + const watcher = fs.watchFile(logPath, { interval: 1000 }, (curr, prev) => { + if (curr.size > prev.size) { + const stream = fs.createReadStream(logPath, { + start: prev.size, + end: curr.size - 1, + encoding: 'utf-8' + }); + + stream.on('data', (chunk) => { + ws.send(JSON.stringify({ type: 'log-update', data: chunk })); + }); } }); ws.on('close', () => { - watcher.close(); + watcher.removeAllListeners(); logger.info('Closed WebSocket connection for logs'); }); } else { From 915cc33fe4d9431b1a3b5165b4d97eb44da5e8e9 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 9 Feb 2025 20:09:18 +0100 Subject: [PATCH 119/369] Fix: Make Linter happy --- src/utils/webSocket.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/webSocket.ts b/src/utils/webSocket.ts index 0d412297..cabf3be7 100644 --- a/src/utils/webSocket.ts +++ b/src/utils/webSocket.ts @@ -78,7 +78,6 @@ export function setupWebSocket(server: Server) { } // Read the initial content of the log file - let lastSize = fs.statSync(logPath).size; const history = fs.readFileSync(logPath, 'utf-8'); ws.send(JSON.stringify({ type: 'log-history', data: history })); From 63c396e571789bc1e823a0bfdfbc4281ec4d35fc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Feb 2025 13:25:19 +0100 Subject: [PATCH 120/369] Change: Graph generation logic changed! --- src/handlers/graph.ts | 295 +++++++--------------------------- src/routes/graphs/routes.ts | 14 ++ src/utils/containerService.ts | 60 +++---- 3 files changed, 108 insertions(+), 261 deletions(-) diff --git a/src/handlers/graph.ts b/src/handlers/graph.ts index 6212adf5..61607c14 100644 --- a/src/handlers/graph.ts +++ b/src/handlers/graph.ts @@ -2,257 +2,84 @@ import cytoscape from "cytoscape"; import logger from "../utils/logger"; import { AllContainerData, ContainerData } from "./../typings/dockerConfig"; import { atomicWrite } from "../utils/atomicWrite"; -import { rateLimitedReadFile } from "../utils/rateLimitFS"; const CACHE_DIR_JSON = "./src/data/graph.json"; -const CACHE_DIR_HTML = "./src/data/graph.html"; -const _assets = "./src/utils/assets"; -const serverSvg = `${_assets}/server-icon.svg`; -const containerSvg = `${_assets}/container-icon.svg`; -const pngPath = "./src/data/graph.png"; -async function getPathData(path: string) { - try { - return await rateLimitedReadFile(path); - - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return false; - } -} - -async function renderGraphToImage( - htmlContent: string, - outputImagePath: string, -): Promise { - let puppeteer; - try { - puppeteer = await import("puppeteer"); - } catch (error) { - logger.error("Puppeteer is not installed. Please install it to generate images."); - throw new Error(`Puppeteer is not installed (${error})`); - } - - let browser; - try { - browser = await puppeteer.default.launch({ - headless: "shell", - args: ["--disable-setuid-sandbox", "--no-sandbox"], - executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, - }); - - const page = await browser.newPage(); - await page.setContent(htmlContent, { waitUntil: "networkidle0" }); - await page.waitForSelector("#cy", { visible: true, timeout: 15000 }); - - await page.waitForFunction( - () => { - const cyElement = document.querySelector("#cy"); - return cyElement ? cyElement.children.length > 0 : false; - }, - { timeout: 10000 } - ); - - await page.screenshot({ - path: outputImagePath, - type: outputImagePath.endsWith(".jpg") ? "jpeg" : "png", - fullPage: true, - captureBeyondViewport: true, - }); - } catch (error: unknown) { - let errorMessage = "Unknown error occurred during browser operation"; - - if (error instanceof Error) { - errorMessage = error.message; - - // Detect common dependency errors - if (errorMessage.includes("libnss3") || errorMessage.includes("libxcb")) { - errorMessage = `❗ Missing system dependencies (libnss3)`; - } - - // Detect Chrome not found errors - if (errorMessage.includes("Failed to launch")) { - errorMessage = `❗ Chrome not found!`; - } - } - - throw new Error(`Graph rendering failed - ${errorMessage}`); - } finally { - if (browser) { - await browser.close().catch(() => { }); - } - } - - logger.info(`Graph rendered and image saved to - ${outputImagePath}`); -} - -async function generateGraphFiles( +async function generateGraphJSON( allContainerData: AllContainerData, ): Promise { - if (process.env.CI === "true") { - logger.warn("Running inside a CI/CD Action, wont generated graphs"); - return false; - } else { - try { - logger.info("generateGraphFiles >>> Starting generation"); - const graphElements: cytoscape.ElementDefinition[] = []; - - for (const [hostName, containers] of Object.entries(allContainerData)) { - if ("error" in containers) { - // TODO: make error'ed hosts better - // Issue URL: https://github.com/Its4Nik/DockStatAPI/issues/32 - graphElements.push({ + try { + logger.info("generateGraphJSON >>> Starting generation"); + + // Define the new JSON structure + const graphData = { + nodes: [] as cytoscape.ElementDefinition[], + edges: [] as cytoscape.ElementDefinition[], + }; + + for (const [hostName, containers] of Object.entries(allContainerData)) { + if ("error" in containers) { + graphData.nodes.push({ + data: { + id: hostName, + label: `Host: ${hostName} Error: ${containers.error}`, + type: "server", + error: true, + }, + }); + } else { + const containerList = containers as ContainerData[]; + + // Host node with container count and metadata + graphData.nodes.push({ + data: { + id: hostName, + label: `${hostName}\n${containerList.length} Containers`, + type: "server", + hostName, + containerCount: containerList.length, + }, + }); + + for (const container of containerList) { + const { id, ...otherContainerProps } = container; + + graphData.nodes.push({ data: { - id: hostName, - label: `Host: ${hostName} Error: ${containers.error}`, - type: "server", + id: id, + label: `${container.name}\n${container.state.toUpperCase()}`, + type: "container", + parent: hostName, + ...otherContainerProps, }, }); - } else { - const containerList = containers as ContainerData[]; - // host node with container count - graphElements.push({ + // Edge between host and container + graphData.edges.push({ data: { - id: hostName, - label: `${hostName} - ${containerList.length} Containers`, - type: "server", + id: `${hostName}-${container.id}`, + source: hostName, + target: container.id, + connectionType: "host-container", }, }); - - for (const container of containerList) { - // container node - graphElements.push({ - data: { - id: container.id, - label: `${container.name} (${container.state})`, - type: "container", - }, - }); - - // edge between host and container - graphElements.push({ - data: { - source: hostName, - target: container.id, - }, - }); - } } } - - atomicWrite(CACHE_DIR_JSON, JSON.stringify(graphElements, null, 2)); - - const htmlContent = ` - - - - - - Cytoscape Graph - - - - -
- - - - `; - - atomicWrite(CACHE_DIR_HTML, htmlContent); - await renderGraphToImage(htmlContent, pngPath) - .then(() => logger.debug("HTML converted to image successfully!")) - .catch((err) => logger.error("Error:", err)); - - logger.info("generateGraphFiles <<< Files generated successfully"); - return true; - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return false; } + + // Write the new structured JSON to file + atomicWrite(CACHE_DIR_JSON, JSON.stringify(graphData, null, 2)); + logger.info("generateGraphJSON <<< JSON file generated successfully"); + return true; + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + return false; } } -function getGraphFilePaths() { - return { json: CACHE_DIR_JSON, html: CACHE_DIR_HTML }; +function getGraphFilePath() { + return { json: CACHE_DIR_JSON }; } -export { generateGraphFiles, getGraphFilePaths }; +export { generateGraphJSON, getGraphFilePath }; diff --git a/src/routes/graphs/routes.ts b/src/routes/graphs/routes.ts index db532058..bf6c8a99 100644 --- a/src/routes/graphs/routes.ts +++ b/src/routes/graphs/routes.ts @@ -1,6 +1,7 @@ import { Request, Response, Router } from "express"; import { createResponseHandler } from "../../handlers/response"; import path from "path"; +import { rateLimitedReadFile } from "../../utils/rateLimitFS"; const router = Router(); router.get("/", async (req: Request, res: Response) => { @@ -28,4 +29,17 @@ router.get("/image", async (req: Request, res: Response) => { } }); +router.get("/json", async (req: Request, res: Response) => { + const ResponseHandler = createResponseHandler(res); + try { + const data = await rateLimitedReadFile( + path.join(__dirname, "/../../.." + "/src/data/graph.json"), + ); + return ResponseHandler.rawData(data, "Graph JSON fetched"); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + return ResponseHandler.critical(errorMsg); + } +}); + export default router; diff --git a/src/utils/containerService.ts b/src/utils/containerService.ts index 86dc2d38..0bb0a4e7 100644 --- a/src/utils/containerService.ts +++ b/src/utils/containerService.ts @@ -1,18 +1,18 @@ import logger from "./logger"; -import { ContainerInfo, } from "dockerode"; +import { ContainerInfo } from "dockerode"; import { getDockerClient } from "./dockerClient"; import fs from "fs"; import { atomicWrite } from "./atomicWrite"; const configPath = "./src/data/dockerConfig.json"; import { AllContainerData, HostConfig } from "../typings/dockerConfig"; -import { generateGraphFiles } from "../handlers/graph"; +import { generateGraphJSON } from "../handlers/graph"; import { WebSocket } from "ws"; export function loadConfig() { try { if (!fs.existsSync(configPath)) { logger.warn( - `Config file not found. Creating an empty file at ${configPath}` + `Config file not found. Creating an empty file at ${configPath}`, ); atomicWrite(configPath, JSON.stringify({ hosts: [] }, null, 2)); } @@ -37,7 +37,9 @@ export async function fetchContainersForHost(hostName: string) { try { const docker = getDockerClient(hostName); - const containers: ContainerInfo[] = await docker.listContainers({ all: true }); + const containers: ContainerInfo[] = await docker.listContainers({ + all: true, + }); return await Promise.all( containers.map(async (container) => { @@ -56,7 +58,8 @@ export async function fetchContainersForHost(hostName: string) { containerStats.precpu_stats.system_cpu_usage; const cpuUsage = systemCpuDelta > 0 - ? (cpuDelta / systemCpuDelta) * containerStats.cpu_stats.online_cpus + ? (cpuDelta / systemCpuDelta) * + containerStats.cpu_stats.online_cpus : 0; return { @@ -90,7 +93,7 @@ export async function fetchContainersForHost(hostName: string) { networkMode: "unknown", }; } - }) + }), ); } catch (error) { logger.error(`Error fetching containers for ${hostName}: ${error}`); @@ -105,16 +108,18 @@ export async function fetchAllContainers(): Promise { await Promise.all( config.hosts.map(async (hostConfig: HostConfig) => { try { - allContainerData[hostConfig.name] = await fetchContainersForHost(hostConfig.name); + allContainerData[hostConfig.name] = await fetchContainersForHost( + hostConfig.name, + ); } catch (error) { allContainerData[hostConfig.name] = { - error: `Error fetching containers: ${error instanceof Error ? error.message : String(error)}` + error: `Error fetching containers: ${error instanceof Error ? error.message : String(error)}`, }; } - }) + }), ); - generateGraphFiles(allContainerData); + generateGraphJSON(allContainerData); return allContainerData; } @@ -127,12 +132,14 @@ export async function streamContainerData(ws: WebSocket, hostName: string) { const eventStream = await docker.getEvents(); // eslint-disable-next-line - if (!(eventStream instanceof require('stream').Readable)) { - throw new Error('Failed to get valid event stream'); + if (!(eventStream instanceof require("stream").Readable)) { + throw new Error("Failed to get valid event stream"); } const handleData = (chunk: Buffer) => { - ws.send(JSON.stringify({ type: "container-event", data: chunk.toString() })); + ws.send( + JSON.stringify({ type: "container-event", data: chunk.toString() }), + ); }; const handleError = (err: Error) => { @@ -140,28 +147,27 @@ export async function streamContainerData(ws: WebSocket, hostName: string) { ws.close(); }; - eventStream - .on('data', handleData) - .on('error', handleError); + eventStream.on("data", handleData).on("error", handleError); const closeHandler = () => { eventStream - .removeListener('data', handleData) - .removeListener('error', handleError) - .removeListener('closed', handleError); + .removeListener("data", handleData) + .removeListener("error", handleError) + .removeListener("closed", handleError); logger.info(`Closed event stream for ${hostName}`); }; - ws.on('close', closeHandler); - ws.on('error', closeHandler); - + ws.on("close", closeHandler); + ws.on("error", closeHandler); } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error("Container data error:", message); - ws.send(JSON.stringify({ - error: "Failed to fetch container data", - details: message - })); + ws.send( + JSON.stringify({ + error: "Failed to fetch container data", + details: message, + }), + ); ws.close(); } -} \ No newline at end of file +} From 3f6792325b5f7a2e22b72d25ee0671ec43f9a5ee Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 12 Feb 2025 13:14:27 +0100 Subject: [PATCH 121/369] Fix: Code styling --- .github/workflows/build-image.yaml | 2 +- .github/workflows/validation.yaml | 2 +- CREDITS.md | 43 +++--- TODO.md | 4 +- __tests__/auth.spec.ts | 4 +- __tests__/config.spec.ts | 2 +- __tests__/database.spec.ts | 2 +- __tests__/frontend.spec.ts | 4 +- __tests__/getters.spec.ts | 2 +- src/handlers/graph.ts | 1 - src/handlers/stack.ts | 2 +- src/utils/dockerClient.ts | 2 +- src/utils/extractHostData.ts | 8 +- src/utils/startServer.ts | 6 +- src/utils/webSocket.ts | 208 +++++++++++++++-------------- 15 files changed, 148 insertions(+), 144 deletions(-) diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index bbb4875d..9d43ff17 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - + - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 7e2b685c..52797fcc 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -1,6 +1,6 @@ name: "Run all tests" -on: +on: push: release: types: diff --git a/CREDITS.md b/CREDITS.md index 50b66abb..6dd2d893 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -20,35 +20,37 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) | ------------------------------------ | ------------------------------------------------------------------------ | -------------------- | | @ampproject/remapping@2.3.0 | https://github.com/ampproject/remapping | Justin Ridgewell | | @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.9.1 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.4 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/config-array@0.19.2 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.10.0 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/core@0.11.0 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/object-schema@2.1.6 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @eslint/plugin-kit@0.2.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | +| @grpc/grpc-js@1.12.6 | https://github.com/grpc/grpc-node/tree/master/packages/grpc-js | Google Inc. | +| @grpc/proto-loader@0.7.13 | https://github.com/grpc/grpc-node | Google Inc. | | @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | | @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | | @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | | @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | | @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @puppeteer/browsers@2.7.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/browsers | The Chromium Authors | +| @puppeteer/browsers@2.7.1 | https://github.com/puppeteer/puppeteer/tree/main/packages/browsers | The Chromium Authors | | @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | | @sigstore/bundle@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | | @sigstore/core@2.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | -| @sigstore/protobuf-specs@0.3.2 | https://github.com/sigstore/protobuf-specs | bdehamer@github.com | +| @sigstore/protobuf-specs@0.3.3 | https://github.com/sigstore/protobuf-specs | bdehamer@github.com | | @sigstore/sign@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | | @sigstore/tuf@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | | @sigstore/verify@2.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | | b4a@1.6.7 | https://github.com/holepunchto/b4a | Holepunch | | bare-events@2.5.4 | https://github.com/holepunchto/bare-events | Holepunch | -| bare-fs@2.3.5 | https://github.com/holepunchto/bare-fs | Holepunch | -| bare-os@2.4.4 | https://github.com/holepunchto/bare-os | Holepunch | -| bare-path@2.1.3 | https://github.com/holepunchto/bare-path | Holepunch | -| bare-stream@2.6.1 | https://github.com/holepunchto/bare-stream | Holepunch | +| bare-fs@4.0.1 | https://github.com/holepunchto/bare-fs | Holepunch | +| bare-os@3.4.0 | https://github.com/holepunchto/bare-os | Holepunch | +| bare-path@3.0.0 | https://github.com/holepunchto/bare-path | Holepunch | +| bare-stream@2.6.5 | https://github.com/holepunchto/bare-stream | Holepunch | | bser@2.1.1 | https://github.com/facebook/watchman | Wez Furlong | -| chromium-bidi@0.11.0 | https://github.com/GoogleChromeLabs/chromium-bidi | The Chromium Authors | -| chromium-bidi@0.12.0 | https://github.com/GoogleChromeLabs/chromium-bidi | The Chromium Authors | +| chromium-bidi@1.2.0 | https://github.com/GoogleChromeLabs/chromium-bidi | The Chromium Authors | | detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.3 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.2 | https://github.com/apocas/dockerode | Pedro Dias | +| docker-modem@5.0.6 | https://github.com/apocas/docker-modem | Pedro Dias | +| dockerode@4.0.4 | https://github.com/apocas/dockerode | Pedro Dias | | ejs@3.1.10 | https://github.com/mde/ejs | Matthew Eernisse | | eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | | eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | @@ -57,14 +59,15 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) | filelist@1.0.4 | https://github.com/mde/filelist | Matthew Eernisse | | human-signals@2.1.0 | https://github.com/ehmicky/human-signals | ehmicky | | jake@10.9.2 | https://github.com/jakejs/jake | Matthew Eernisse | -| puppeteer-core@24.0.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core | The Chromium Authors | -| puppeteer@24.0.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer | The Chromium Authors | +| long@5.2.4 | https://github.com/dcodeIO/long.js | Daniel Wirtz | +| puppeteer-core@24.2.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core | The Chromium Authors | +| puppeteer@24.2.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer | The Chromium Authors | | sigstore@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | | spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.2 | https://github.com/swagger-api/swagger-ui | N/A | +| swagger-ui-dist@5.18.3 | https://github.com/swagger-api/swagger-ui | N/A | | text-decoder@1.2.3 | https://github.com/holepunchto/text-decoder | Holepunch | | tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.2 | https://github.com/microsoft/TypeScript | Microsoft Corp. | +| typescript@5.7.3 | https://github.com/microsoft/TypeScript | Microsoft Corp. | | validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | | walker@1.0.8 | https://github.com/daaku/nodejs-walker | Naitik Shah | @@ -72,7 +75,7 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) | Name | Repository | Publisher | | ---------- | -------------------------- | ----------- | -| npm@11.0.0 | https://github.com/npm/cli | GitHub Inc. | +| npm@11.1.0 | https://github.com/npm/cli | GitHub Inc. | ### License: BlueOak-1.0.0 @@ -94,7 +97,7 @@ This file shows all npm packages used in DockStatAPI (also Dev packages) | Name | Repository | Publisher | | ------------------------- | -------------------------------------------- | ---------- | -| caniuse-lite@1.0.30001690 | https://github.com/browserslist/caniuse-lite | Ben Briggs | +| caniuse-lite@1.0.30001698 | https://github.com/browserslist/caniuse-lite | Ben Briggs | ### License: Python-2.0 diff --git a/TODO.md b/TODO.md index 44a128d3..b850ba72 100644 --- a/TODO.md +++ b/TODO.md @@ -7,12 +7,12 @@ - [x] Structure code differently - [x] Write new README and make the docs better - [x] Update more files to correct TS syntax => remove "any" -- [X] Websockets +- [x] Websockets - [x] Better /api/status endpoint with connection status of each host - [x] Update notification service - [x] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) - [ ] Better project structure - [x] Update logging => Better errors - [x] Update json responses -- [X] Swagger update +- [x] Swagger update - [ ] Edge case testing diff --git a/__tests__/auth.spec.ts b/__tests__/auth.spec.ts index bcf0eb21..84c5f04a 100644 --- a/__tests__/auth.spec.ts +++ b/__tests__/auth.spec.ts @@ -1,5 +1,5 @@ export const testPass = "123456789"; -import { Server } from 'http'; +import { Server } from "http"; import supertest from "supertest"; import { startServer } from "../src/utils/startServer"; import app from "../src/server"; @@ -35,4 +35,4 @@ describe("Authentication", () => { expect(res.status).toEqual(200); expect(res.type).toEqual(expect.stringContaining("json")); }); -}); \ No newline at end of file +}); diff --git a/__tests__/config.spec.ts b/__tests__/config.spec.ts index d6356004..2650e9ed 100644 --- a/__tests__/config.spec.ts +++ b/__tests__/config.spec.ts @@ -1,7 +1,7 @@ import supertest from "supertest"; import { startServer } from "../src/utils/startServer"; import app from "../src/server"; -import { Server } from 'http'; +import { Server } from "http"; const port = 13002; const server = new Server(app); diff --git a/__tests__/database.spec.ts b/__tests__/database.spec.ts index c0c46c1b..55102ce9 100644 --- a/__tests__/database.spec.ts +++ b/__tests__/database.spec.ts @@ -1,7 +1,7 @@ import supertest from "supertest"; import { startServer } from "../src/utils/startServer"; import app from "../src/server"; -import { Server } from 'http'; +import { Server } from "http"; const port = 13003; const server = new Server(app); diff --git a/__tests__/frontend.spec.ts b/__tests__/frontend.spec.ts index 753b98da..af25adc5 100644 --- a/__tests__/frontend.spec.ts +++ b/__tests__/frontend.spec.ts @@ -1,7 +1,7 @@ import supertest from "supertest"; import { startServer } from "../src/utils/startServer"; import app from "../src/server"; -import { Server } from 'http'; +import { Server } from "http"; const port = 13004; const server = new Server(app); @@ -29,8 +29,6 @@ const verifiedResponse = [ }, ]; - - describe("Test frontend specific configurations", () => { it( "Setup the configuration file", diff --git a/__tests__/getters.spec.ts b/__tests__/getters.spec.ts index 3ba5950b..f951f42a 100644 --- a/__tests__/getters.spec.ts +++ b/__tests__/getters.spec.ts @@ -2,7 +2,7 @@ import { createPreviousResponse } from "./util/previousResponse"; import supertest from "supertest"; import { startServer } from "../src/utils/startServer"; import app from "../src/server"; -import { Server } from 'http'; +import { Server } from "http"; const port = 13005; const server = new Server(app); diff --git a/src/handlers/graph.ts b/src/handlers/graph.ts index 61607c14..587d5760 100644 --- a/src/handlers/graph.ts +++ b/src/handlers/graph.ts @@ -49,7 +49,6 @@ async function generateGraphJSON( id: id, label: `${container.name}\n${container.state.toUpperCase()}`, type: "container", - parent: hostName, ...otherContainerProps, }, }); diff --git a/src/handlers/stack.ts b/src/handlers/stack.ts index e87b533f..b3daa0f8 100644 --- a/src/handlers/stack.ts +++ b/src/handlers/stack.ts @@ -107,7 +107,7 @@ class StackHandler { async stackCompose(req: Request, res: Response) { const ResponseHandler = createResponseHandler(res); try { - const {name} = req.params; + const { name } = req.params; return ResponseHandler.rawData( await getStackCompose(name), "Stack compose fetched", diff --git a/src/utils/dockerClient.ts b/src/utils/dockerClient.ts index 469c4096..ff770888 100644 --- a/src/utils/dockerClient.ts +++ b/src/utils/dockerClient.ts @@ -18,7 +18,7 @@ function loadDockerConfig(): dockerConfig { function createDockerClient(hostConfig: target): Docker { logger.info( - `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port || 2375}` + `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port || 2375}`, ); return new Docker({ host: hostConfig.url, diff --git a/src/utils/extractHostData.ts b/src/utils/extractHostData.ts index a383dc00..992f9638 100644 --- a/src/utils/extractHostData.ts +++ b/src/utils/extractHostData.ts @@ -29,13 +29,13 @@ function processComponents(components: unknown): ComponentMap { return components.reduce((acc, component) => { if ( - typeof component === 'object' && + typeof component === "object" && component !== null && - 'Name' in component && - 'Version' in component + "Name" in component && + "Version" in component ) { const { Name, Version } = component; - if (typeof Name === 'string' && typeof Version === 'string') { + if (typeof Name === "string" && typeof Version === "string") { acc[Name] = Version; } } diff --git a/src/utils/startServer.ts b/src/utils/startServer.ts index 7ca612f8..52dcc256 100644 --- a/src/utils/startServer.ts +++ b/src/utils/startServer.ts @@ -1,18 +1,16 @@ import { Express } from "express"; -import { Server } from 'http'; +import { Server } from "http"; import { startMasterNode } from "../controllers/highAvailability"; import writeUserConf from "../config/hostsystem"; import initFiles from "../config/initFiles"; - export function startServer(app: Express, server: Server, port: number) { if (process.env.NODE_ENV === "testing") { writeUserConf(port); initFiles(); } - server.listen(port, () => { startMasterNode(); }); -} \ No newline at end of file +} diff --git a/src/utils/webSocket.ts b/src/utils/webSocket.ts index cabf3be7..66d1f74b 100644 --- a/src/utils/webSocket.ts +++ b/src/utils/webSocket.ts @@ -1,107 +1,113 @@ -import { Server } from 'http'; -import { WebSocketServer, WebSocket } from 'ws'; -import { URL } from 'url'; -import fs from 'fs'; +import { Server } from "http"; +import { WebSocketServer, WebSocket } from "ws"; +import { URL } from "url"; +import fs from "fs"; import logger from "./logger"; -import { streamContainerData } from './containerService'; +import { streamContainerData } from "./containerService"; export function setupWebSocket(server: Server) { - const wss = new WebSocketServer({ noServer: true }); - - server.on('upgrade', (req, socket, head) => { - logger.debug(`Received upgrade request for URL: ${req.url}`); - const baseURL = `http://${req.headers.host}/`; - const requestURL = new URL(req.url || '', baseURL); - const { pathname } = requestURL; - logger.debug(`Parsed pathname: ${pathname}`); - - // Debug log to verify path handling - logger.debug(`Handling upgrade for path: ${pathname}`); - - if (pathname === '/wss/container-data' || pathname === '/wss/server-logs') { - wss.handleUpgrade(req, socket, head, (ws) => { - wss.emit('connection', ws, req); - }); - } else { - logger.warn(`Rejected WebSocket connection to invalid path: ${pathname}`); - socket.write('HTTP/1.1 404 Not Found\r\n\r\n'); - socket.destroy(); - } - }); - - server.on("error", (error) => { - logger.error("HTTP server error:", error); - }); - - logger.debug("WebSocket server attached to HTTP server"); - - wss.on('connection', (ws: WebSocket, req) => { - const baseURL = `http://${req.headers.host}/`; - const requestURL = new URL(req.url || '', baseURL); - const { pathname } = requestURL; - - logger.info(`WebSocket connection established to ${pathname}`); - - const handleError = (error: string) => { - ws.send(JSON.stringify({ error })); - ws.close(); - }; - - if (pathname === '/wss/container-data') { - const hostName = requestURL.searchParams.get('host'); - if (!hostName) { - handleError('Missing required host parameter'); - return; - } - streamContainerData(ws, hostName); - } else if (pathname === '/wss/server-logs') { - const logFiles = fs.readdirSync("logs/").filter(file => file.startsWith('app-')); - - if (logFiles.length === 0) { - console.error('No log files found'); - return; - } - - const sortedLogFiles = logFiles.sort((a, b) => { - const dateA = a.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; - const dateB = b.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; - - return dateB.localeCompare(dateA); - }); - - const logPath = "logs/" + sortedLogFiles[0]; - - if (!fs.existsSync(logPath)) { - handleError('Log file not found'); - logger.error(`Log file ${logPath} not found`) - return; - } - - // Read the initial content of the log file - const history = fs.readFileSync(logPath, 'utf-8'); - ws.send(JSON.stringify({ type: 'log-history', data: history })); - - // Watch the log file for changes - const watcher = fs.watchFile(logPath, { interval: 1000 }, (curr, prev) => { - if (curr.size > prev.size) { - const stream = fs.createReadStream(logPath, { - start: prev.size, - end: curr.size - 1, - encoding: 'utf-8' - }); - - stream.on('data', (chunk) => { - ws.send(JSON.stringify({ type: 'log-update', data: chunk })); - }); - } + const wss = new WebSocketServer({ noServer: true }); + + server.on("upgrade", (req, socket, head) => { + logger.debug(`Received upgrade request for URL: ${req.url}`); + const baseURL = `http://${req.headers.host}/`; + const requestURL = new URL(req.url || "", baseURL); + const { pathname } = requestURL; + logger.debug(`Parsed pathname: ${pathname}`); + + // Debug log to verify path handling + logger.debug(`Handling upgrade for path: ${pathname}`); + + if (pathname === "/wss/container-data" || pathname === "/wss/server-logs") { + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit("connection", ws, req); + }); + } else { + logger.warn(`Rejected WebSocket connection to invalid path: ${pathname}`); + socket.write("HTTP/1.1 404 Not Found\r\n\r\n"); + socket.destroy(); + } + }); + + server.on("error", (error) => { + logger.error("HTTP server error:", error); + }); + + logger.debug("WebSocket server attached to HTTP server"); + + wss.on("connection", (ws: WebSocket, req) => { + const baseURL = `http://${req.headers.host}/`; + const requestURL = new URL(req.url || "", baseURL); + const { pathname } = requestURL; + + logger.info(`WebSocket connection established to ${pathname}`); + + const handleError = (error: string) => { + ws.send(JSON.stringify({ error })); + ws.close(); + }; + + if (pathname === "/wss/container-data") { + const hostName = requestURL.searchParams.get("host"); + if (!hostName) { + handleError("Missing required host parameter"); + return; + } + streamContainerData(ws, hostName); + } else if (pathname === "/wss/server-logs") { + const logFiles = fs + .readdirSync("logs/") + .filter((file) => file.startsWith("app-")); + + if (logFiles.length === 0) { + console.error("No log files found"); + return; + } + + const sortedLogFiles = logFiles.sort((a, b) => { + const dateA = a.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; + const dateB = b.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; + + return dateB.localeCompare(dateA); + }); + + const logPath = "logs/" + sortedLogFiles[0]; + + if (!fs.existsSync(logPath)) { + handleError("Log file not found"); + logger.error(`Log file ${logPath} not found`); + return; + } + + // Read the initial content of the log file + const history = fs.readFileSync(logPath, "utf-8"); + ws.send(JSON.stringify({ type: "log-history", data: history })); + + // Watch the log file for changes + const watcher = fs.watchFile( + logPath, + { interval: 1000 }, + (curr, prev) => { + if (curr.size > prev.size) { + const stream = fs.createReadStream(logPath, { + start: prev.size, + end: curr.size - 1, + encoding: "utf-8", }); - ws.on('close', () => { - watcher.removeAllListeners(); - logger.info('Closed WebSocket connection for logs'); + stream.on("data", (chunk) => { + ws.send(JSON.stringify({ type: "log-update", data: chunk })); }); - } else { - handleError('Invalid WebSocket endpoint'); - } - }); -} \ No newline at end of file + } + }, + ); + + ws.on("close", () => { + watcher.removeAllListeners(); + logger.info("Closed WebSocket connection for logs"); + }); + } else { + handleError("Invalid WebSocket endpoint"); + } + }); +} From d7c80167cfc64d525454ea9ae0a6811c022a89cb Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 15 Feb 2025 19:13:17 +0100 Subject: [PATCH 122/369] Fix: Remove unusable routes --- src/config/swagger.yaml | 11 ----------- src/handlers/graph.ts | 2 -- src/routes/graphs/routes.ts | 25 ------------------------- 3 files changed, 38 deletions(-) diff --git a/src/config/swagger.yaml b/src/config/swagger.yaml index 9a1d50fb..2230f73b 100644 --- a/src/config/swagger.yaml +++ b/src/config/swagger.yaml @@ -33,17 +33,6 @@ info: - Multi Arch Docker builds through docker buildx - High Availability using single master and unlimited worker nodes! -
- Your container graph - [Interactive Graph](http://localhost:9876/graph) - - [Raw image](http://localhost:9876/graph/image) - - --- - - ![Your container graph](http://localhost:9876/graph/image) -
- # 🔗 DockStatAPI v2 Documentation _⚠️ = Deprecation warning_ diff --git a/src/handlers/graph.ts b/src/handlers/graph.ts index 587d5760..12e05724 100644 --- a/src/handlers/graph.ts +++ b/src/handlers/graph.ts @@ -11,7 +11,6 @@ async function generateGraphJSON( try { logger.info("generateGraphJSON >>> Starting generation"); - // Define the new JSON structure const graphData = { nodes: [] as cytoscape.ElementDefinition[], edges: [] as cytoscape.ElementDefinition[], @@ -66,7 +65,6 @@ async function generateGraphJSON( } } - // Write the new structured JSON to file atomicWrite(CACHE_DIR_JSON, JSON.stringify(graphData, null, 2)); logger.info("generateGraphJSON <<< JSON file generated successfully"); return true; diff --git a/src/routes/graphs/routes.ts b/src/routes/graphs/routes.ts index bf6c8a99..fcaa7983 100644 --- a/src/routes/graphs/routes.ts +++ b/src/routes/graphs/routes.ts @@ -4,31 +4,6 @@ import path from "path"; import { rateLimitedReadFile } from "../../utils/rateLimitFS"; const router = Router(); -router.get("/", async (req: Request, res: Response) => { - const ResponseHandler = createResponseHandler(res); - try { - const graphPath = path.join( - __dirname, - "/../../.." + "/src/data/graph.html", - ); - return res.contentType("html").status(200).sendFile(graphPath); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } -}); - -router.get("/image", async (req: Request, res: Response) => { - const ResponseHandler = createResponseHandler(res); - try { - const graphPath = path.join(__dirname, "/../../.." + "/src/data/graph.png"); - return res.contentType("image/png").status(200).sendFile(graphPath); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } -}); - router.get("/json", async (req: Request, res: Response) => { const ResponseHandler = createResponseHandler(res); try { From 62b05740c178821753c4ef609755e3c57f133c5a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 15 Feb 2025 21:55:51 +0100 Subject: [PATCH 123/369] Fix: Add docker executable --- docker/Dockerfile-base | 5 +++-- docker/Dockerfile-dev | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 76cec4c9..d135eaa4 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -30,8 +30,9 @@ FROM node:20-alpine AS production WORKDIR /api -RUN apk add --no-cache bash curl && \ - adduser -h /api -s /bin/bash -D dockstatapi +RUN apk add --no-cache bash curl docker-cli && \ + adduser -h /api -s /bin/bash -D dockstatapi && \ + addgroup dockstatapi docker HEALTHCHECK --interval=5m --timeout=3s \ CMD curl -f http://localhost:9876/api/status || exit 1 diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index 43a42402..bcc54e42 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -30,6 +30,10 @@ FROM node:20-alpine AS production WORKDIR /api +RUN apk add --no-cache bash curl docker-cli && \ + adduser -h /api -s /bin/bash -D dockstatapi && \ + addgroup dockstatapi docker + RUN apk add --no-cache bash curl && \ adduser -h /api -s /bin/bash -D dockstatapi From 01d73071c6b6fa6ab29e39207a9aef20657ddbd5 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 15 Feb 2025 22:06:49 +0100 Subject: [PATCH 124/369] Fix: Fixing user creation and docker user --- docker/Dockerfile-base | 14 +++++++------- docker/Dockerfile-dev | 17 +++++++---------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index d135eaa4..80b1ae8f 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -15,9 +15,7 @@ WORKDIR /app ENV NODE_NO_WARNINGS=1 -RUN apk add --no-cache bash - -COPY tsconfig.json environment.d.ts package*.json ./ +COPY package*.json tsconfig.json environment.d.ts ./ RUN npm install --production=false @@ -30,8 +28,9 @@ FROM node:20-alpine AS production WORKDIR /api -RUN apk add --no-cache bash curl docker-cli && \ - adduser -h /api -s /bin/bash -D dockstatapi && \ +RUN apk add --no-cache docker-cli bash curl && \ + adduser -h /api -s /bin/sh -D dockstatapi && \ + addgroup -S docker && \ addgroup dockstatapi docker HEALTHCHECK --interval=5m --timeout=3s \ @@ -42,7 +41,8 @@ COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/config/swagger.yaml /api/src/config/swagger.yaml COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/utils/assets /api/src/utils/assets -RUN npm install --omit=dev +RUN npm install --omit=dev && \ + rm -rf package-lock.json node_modules/.cache COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh @@ -56,4 +56,4 @@ RUN mkdir -p /api/src/data && \ STOPSIGNAL 130 USER dockstatapi -ENTRYPOINT [ "bash", "./entrypoint.sh", "--prod" ] +ENTRYPOINT [ "sh", "./entrypoint.sh", "--prod" ] diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index bcc54e42..7b439404 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -15,9 +15,7 @@ WORKDIR /app ENV NODE_NO_WARNINGS=1 -RUN apk add --no-cache bash - -COPY tsconfig.json environment.d.ts package*.json ./ +COPY package*.json tsconfig.json environment.d.ts ./ RUN npm install --production=false @@ -30,13 +28,11 @@ FROM node:20-alpine AS production WORKDIR /api -RUN apk add --no-cache bash curl docker-cli && \ - adduser -h /api -s /bin/bash -D dockstatapi && \ +RUN apk add --no-cache docker-cli bash curl && \ + adduser -h /api -s /bin/sh -D dockstatapi && \ + addgroup -S docker && \ addgroup dockstatapi docker -RUN apk add --no-cache bash curl && \ - adduser -h /api -s /bin/bash -D dockstatapi - HEALTHCHECK --interval=5m --timeout=3s \ CMD curl -f http://localhost:9876/api/status || exit 1 @@ -45,7 +41,8 @@ COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/config/swagger.yaml /api/src/config/swagger.yaml COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/utils/assets /api/src/utils/assets -RUN npm install +RUN npm install --omit=dev && \ + rm -rf package-lock.json node_modules/.cache COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh @@ -59,4 +56,4 @@ RUN mkdir -p /api/src/data && \ STOPSIGNAL 130 USER dockstatapi -ENTRYPOINT [ "bash", "./entrypoint.sh", "--dev" ] +ENTRYPOINT [ "sh", "./entrypoint.sh", "--dev" ] From 955d41363e0d3057dd23b24f55f89fe49c2ce4cd Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 15 Feb 2025 22:12:11 +0100 Subject: [PATCH 125/369] Fix: Use Node-20-slim in build stage --- docker/Dockerfile-base | 2 +- docker/Dockerfile-dev | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 80b1ae8f..8dd89293 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -1,5 +1,5 @@ # Stage 1: Build stage -FROM node:20-alpine AS builder +FROM node:20-slim AS builder LABEL maintainer="https://github.com/its4nik" LABEL version="2.0.1" diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index 7b439404..f3f3caea 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -1,5 +1,5 @@ # Stage 1: Build stage -FROM node:20-alpine AS builder +FROM node:20-slim AS builder LABEL maintainer="https://github.com/its4nik" LABEL version="2.0.1" From fdac5739dafcea9ac2f14cea8a5c29b5172699d5 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 15 Feb 2025 22:53:43 +0100 Subject: [PATCH 126/369] Fix: Update to composeAction --- src/handlers/stack.ts | 48 ++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/handlers/stack.ts b/src/handlers/stack.ts index b3daa0f8..ad36373e 100644 --- a/src/handlers/stack.ts +++ b/src/handlers/stack.ts @@ -27,29 +27,35 @@ export async function validate(name: string): Promise { async function composeAction(option: string, name: string): Promise { const composeFile: string = path.join(PROJECT_ROOT, `stacks/${name}`); - switch (option) { - case "start": { - await compose.upAll({ cwd: composeFile, log: false }).then( - () => { - return true; - }, - (err: unknown) => { - throw new Error(err as string); - }, - ); - break; + try { + switch (option) { + case "start": { + await compose.upAll({ cwd: composeFile, log: false }); + break; + } + case "stop": { + await compose.downAll({ cwd: composeFile, log: false }); + break; + } + default: + throw new Error(`Invalid option: ${option}`); } - case "stop": { - await compose.downAll({ cwd: composeFile, log: false }).then( - () => { - return true; - }, - (err: unknown) => { - throw new Error(err as string); - }, - ); - break; + } catch (err) { + let errorMessage: string; + const portAllocated: string = "port is already allocated"; + + if (err instanceof Error) { + errorMessage = err.message; + } else if (typeof err === "object" && err !== null) { + errorMessage = JSON.stringify(err); + } else { + errorMessage = String(err); + } + + if (errorMessage.search(portAllocated)) { + errorMessage = "Port(s) already allocated"; } + throw new Error(errorMessage); } } From caa4b6ce6ae58a766fea42a1ea27d1f6ae5b6106 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 15 Feb 2025 23:51:45 +0100 Subject: [PATCH 127/369] Fix: Make errors more verbose for debugging purpose --- src/handlers/stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/stack.ts b/src/handlers/stack.ts index ad36373e..0f15e166 100644 --- a/src/handlers/stack.ts +++ b/src/handlers/stack.ts @@ -53,7 +53,7 @@ async function composeAction(option: string, name: string): Promise { } if (errorMessage.search(portAllocated)) { - errorMessage = "Port(s) already allocated"; + logger.error("Port(s) already allocated"); } throw new Error(errorMessage); } From fbc63f4e06124facc2ece85507c80ead12da1269 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 16 Feb 2025 00:23:08 +0100 Subject: [PATCH 128/369] Fix: Add correct docker packages to Container --- docker/Dockerfile-base | 2 +- docker/Dockerfile-dev | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 8dd89293..296dfe17 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -28,7 +28,7 @@ FROM node:20-alpine AS production WORKDIR /api -RUN apk add --no-cache docker-cli bash curl && \ +RUN apk add --no-cache docker docker-compose bash curl && \ adduser -h /api -s /bin/sh -D dockstatapi && \ addgroup -S docker && \ addgroup dockstatapi docker diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index f3f3caea..d0bd6236 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -28,7 +28,7 @@ FROM node:20-alpine AS production WORKDIR /api -RUN apk add --no-cache docker-cli bash curl && \ +RUN apk add --no-cache docker docker-compose bash curl && \ adduser -h /api -s /bin/sh -D dockstatapi && \ addgroup -S docker && \ addgroup dockstatapi docker From 0dc912eb9b35dc72531b7039455fdab03f816fbc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 16 Feb 2025 00:40:44 +0100 Subject: [PATCH 129/369] Fix: Adjust dockerfile (gosh i hope it works now) --- docker/Dockerfile-base | 62 ++++++++++++++++++++++++++---------------- docker/Dockerfile-dev | 62 ++++++++++++++++++++++++++---------------- 2 files changed, 78 insertions(+), 46 deletions(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index 296dfe17..bfee3a3d 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -1,5 +1,5 @@ # Stage 1: Build stage -FROM node:20-slim AS builder +FROM node:20-alpine AS builder LABEL maintainer="https://github.com/its4nik" LABEL version="2.0.1" @@ -15,45 +15,61 @@ WORKDIR /app ENV NODE_NO_WARNINGS=1 +RUN apk add --no-cache curl bash + COPY package*.json tsconfig.json environment.d.ts ./ -RUN npm install --production=false +RUN npm ci --include=dev COPY ./src ./src RUN mv ./src/sample-variable.json ./src/data/variables.json -RUN npm run build:mini -# Stage 2: Production stage -FROM node:20-alpine AS production +RUN npm run build:mini +# -------------------------------------- +# Stage 2: Dependency pruning stage +FROM node:20-alpine AS deps WORKDIR /api +COPY --from=builder /app/package*.json . +RUN npm ci --omit=dev -RUN apk add --no-cache docker docker-compose bash curl && \ - adduser -h /api -s /bin/sh -D dockstatapi && \ - addgroup -S docker && \ - addgroup dockstatapi docker +# -------------------------------------- +# Stage 3: Final production image +FROM node:20-alpine AS prod -HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:9876/api/status || exit 1 +WORKDIR /api -COPY --chown=dockstatapi:dockstatapi --from=builder /app/dist/src /api/src -COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/config/swagger.yaml /api/src/config/swagger.yaml -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/utils/assets /api/src/utils/assets +RUN apk add --no-cache docker-cli bash curl && \ + mkdir -p /usr/libexec/docker/cli-plugins && \ + curl -sSL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" \ + -o /usr/libexec/docker/cli-plugins/docker-compose && \ + chmod +x /usr/libexec/docker/cli-plugins/docker-compose && \ + rm -rf /var/cache/apk/* -RUN npm install --omit=dev && \ - rm -rf package-lock.json node_modules/.cache +ARG USER_ID=10001 +ARG GROUP_ID=10001 +RUN addgroup -g $GROUP_ID dockstatapi && \ + adduser -u $USER_ID -G dockstatapi -h /api -s /bin/sh -D dockstatapi -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh -RUN chmod +x /api/*.sh +COPY --from=builder --chown=dockstatapi:dockstatapi /app/dist/src ./src +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/config/swagger.yaml ./src/config/swagger.yaml +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/utils/assets ./src/utils/assets +COPY --from=deps --chown=dockstatapi:dockstatapi /api/node_modules ./node_modules -EXPOSE 9876 +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/entrypoint.sh . +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/createEnvFile.sh . +RUN chmod +x *.sh RUN mkdir -p /api/src/data && \ - chmod -R 777 /api/src/data /api && \ - chown -R dockstatapi:dockstatapi /api + chown -R dockstatapi:dockstatapi /api && \ + chmod -R 755 /api && \ + chmod 775 /api/src/data + +HEALTHCHECK --interval=5m --timeout=3s \ + CMD curl -f http://localhost:9876/api/status || exit 1 +EXPOSE 9876 STOPSIGNAL 130 USER dockstatapi + ENTRYPOINT [ "sh", "./entrypoint.sh", "--prod" ] diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index d0bd6236..dfda5354 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -1,5 +1,5 @@ # Stage 1: Build stage -FROM node:20-slim AS builder +FROM node:20-alpine AS builder LABEL maintainer="https://github.com/its4nik" LABEL version="2.0.1" @@ -15,45 +15,61 @@ WORKDIR /app ENV NODE_NO_WARNINGS=1 +RUN apk add --no-cache curl bash + COPY package*.json tsconfig.json environment.d.ts ./ -RUN npm install --production=false +RUN npm ci --include=dev COPY ./src ./src RUN mv ./src/sample-variable.json ./src/data/variables.json -RUN npm run build -# Stage 2: Production stage -FROM node:20-alpine AS production +RUN npm run build +# -------------------------------------- +# Stage 2: Dependency pruning stage +FROM node:20-alpine AS deps WORKDIR /api +COPY --from=builder /app/package*.json . +RUN npm ci --omit=dev -RUN apk add --no-cache docker docker-compose bash curl && \ - adduser -h /api -s /bin/sh -D dockstatapi && \ - addgroup -S docker && \ - addgroup dockstatapi docker +# -------------------------------------- +# Stage 3: Final production image +FROM node:20-alpine AS prod -HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:9876/api/status || exit 1 +WORKDIR /api -COPY --chown=dockstatapi:dockstatapi --from=builder /app/dist/src /api/src -COPY --chown=dockstatapi:dockstatapi --from=builder /app/package*.json /api/ -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/config/swagger.yaml /api/src/config/swagger.yaml -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/utils/assets /api/src/utils/assets +RUN apk add --no-cache docker-cli bash curl && \ + mkdir -p /usr/libexec/docker/cli-plugins && \ + curl -sSL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" \ + -o /usr/libexec/docker/cli-plugins/docker-compose && \ + chmod +x /usr/libexec/docker/cli-plugins/docker-compose && \ + rm -rf /var/cache/apk/* -RUN npm install --omit=dev && \ - rm -rf package-lock.json node_modules/.cache +ARG USER_ID=10001 +ARG GROUP_ID=10001 +RUN addgroup -g $GROUP_ID dockstatapi && \ + adduser -u $USER_ID -G dockstatapi -h /api -s /bin/sh -D dockstatapi -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/entrypoint.sh /api/entrypoint.sh -COPY --chown=dockstatapi:dockstatapi --from=builder /app/src/misc/createEnvFile.sh /api/createEnvFile.sh -RUN chmod +x /api/*.sh +COPY --from=builder --chown=dockstatapi:dockstatapi /app/dist/src ./src +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/config/swagger.yaml ./src/config/swagger.yaml +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/utils/assets ./src/utils/assets +COPY --from=deps --chown=dockstatapi:dockstatapi /api/node_modules ./node_modules -EXPOSE 9876 +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/entrypoint.sh . +COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/createEnvFile.sh . +RUN chmod +x *.sh RUN mkdir -p /api/src/data && \ - chmod -R 777 /api/src/data /api && \ - chown -R dockstatapi:dockstatapi /api + chown -R dockstatapi:dockstatapi /api && \ + chmod -R 755 /api && \ + chmod 775 /api/src/data + +HEALTHCHECK --interval=5m --timeout=3s \ + CMD curl -f http://localhost:9876/api/status || exit 1 +EXPOSE 9876 STOPSIGNAL 130 USER dockstatapi + ENTRYPOINT [ "sh", "./entrypoint.sh", "--dev" ] From c49da92f649c9e78ccd289929168f8aabe6300ff Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 16 Feb 2025 15:46:33 +0100 Subject: [PATCH 130/369] Fix: Adjust entrypoints and dockerfiles --- docker/Dockerfile-base | 1 + docker/Dockerfile-dev | 1 + src/misc/entrypoint.sh | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base index bfee3a3d..f21146ba 100644 --- a/docker/Dockerfile-base +++ b/docker/Dockerfile-base @@ -54,6 +54,7 @@ RUN addgroup -g $GROUP_ID dockstatapi && \ COPY --from=builder --chown=dockstatapi:dockstatapi /app/dist/src ./src COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/config/swagger.yaml ./src/config/swagger.yaml COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/utils/assets ./src/utils/assets +COPY --from=builder /app/package.json ./ COPY --from=deps --chown=dockstatapi:dockstatapi /api/node_modules ./node_modules COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/entrypoint.sh . diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index dfda5354..00b88008 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -54,6 +54,7 @@ RUN addgroup -g $GROUP_ID dockstatapi && \ COPY --from=builder --chown=dockstatapi:dockstatapi /app/dist/src ./src COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/config/swagger.yaml ./src/config/swagger.yaml COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/utils/assets ./src/utils/assets +COPY --from=builder /app/package.json ./ COPY --from=deps --chown=dockstatapi:dockstatapi /api/node_modules ./node_modules COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/entrypoint.sh . diff --git a/src/misc/entrypoint.sh b/src/misc/entrypoint.sh index 77b6236e..b352ca75 100755 --- a/src/misc/entrypoint.sh +++ b/src/misc/entrypoint.sh @@ -1,4 +1,3 @@ -# entrypoint.sh: #!/bin/bash VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" From 9576cdc972022b2968012dc298c82b7635325582 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 16 Feb 2025 15:51:54 +0100 Subject: [PATCH 131/369] Feat: Test new workflow --- .github/workflows/validation.yaml | 238 ++++++++---------------------- 1 file changed, 63 insertions(+), 175 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index 52797fcc..dfa18edf 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -1,20 +1,18 @@ -name: "Run all tests" +name: "CI/CD Pipeline" on: push: release: - types: - - published + types: [published] jobs: validation: + name: "Code Validation & Tests" runs-on: ubuntu-24.04 - name: "Validation" permissions: - security-events: write - packages: read actions: read contents: read + packages: read steps: - name: Checkout uses: actions/checkout@v4 @@ -31,77 +29,37 @@ jobs: - name: Create varaibles.json run: npm run local-env-file - - name: Run prettier + - name: Run code formatting run: npm run prettier - name: Run linter run: npm run lint - - name: Build + - name: Build project run: npm run build:mini - name: Audit packages run: npm audit --audit-level=high - - name: Jests + - name: Run tests run: npm run test:silent - ToDo: - needs: validation - runs-on: ubuntu-20.04 - name: "ToDo comment to issue" - permissions: - contents: write - issues: write - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: "TODO to Issue" - uses: "alstr/todo-to-issue-action@v5" - with: - INSERT_ISSUE_URLS: "true" - - - name: Set Git user - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - - - name: Commit and Push Changes - run: | - git add -A - if [[ `git status --porcelain` ]]; then - git commit -m "Automatically added GitHub issue links to TODOs" - git push - else - echo "No changes to commit" - fi - - CodeQL: - needs: [ToDo] + security-analysis: + name: "Security Analysis" runs-on: ubuntu-24.04 - name: "Analyze TypeScript" + needs: validation permissions: security-events: write - packages: read - actions: read contents: read - + packages: read steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Setup python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: javascript-typescript - build-mode: none queries: security-extended config: | query-filter: @@ -110,194 +68,124 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 - with: - category: "/language:javascript-typescript" - Anchore: - needs: [ToDo] + container-scanning: + name: "Container Security" runs-on: ubuntu-24.04 - name: "Anchore" + needs: validation permissions: security-events: write - packages: read - actions: read contents: read steps: - - name: Set up Grype installation path - run: echo "$HOME/bin" >> $GITHUB_PATH - - - name: Setup python - uses: actions/setup-python@v5 - with: - python-version: "3.13" + - name: Checkout repository + uses: actions/checkout@v4 - name: Download Grype run: | curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin + echo "$HOME/bin" >> $GITHUB_PATH - - uses: actions/checkout@v4 - - - name: Build the Container image + - name: Build Docker image run: docker build . --file docker/Dockerfile-base --tag localbuild/testimage:latest - - name: Run Grype test + - name: Run vulnerability scan run: grype -o sarif localbuild/testimage:latest > results.sarif - - name: Upload Anchore scan SARIF report + - name: Upload SARIF report uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ./results.sarif - test-building: - needs: [ToDo] + build-test: + name: "Docker Build Test" runs-on: ubuntu-24.04 - name: "Test building" + needs: validation permissions: - security-events: write + contents: read packages: read - actions: read - contents: write steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Setup python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Github Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate Docker tags - uses: docker/metadata-action@v5 - id: metadata - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,enable=true,priority=200,prefix=,suffix=,value=${{ github.sha }} - - - name: Build and Push Docker Images + - name: Build Docker image uses: docker/build-push-action@v6 with: context: . file: docker/Dockerfile-base platforms: linux/amd64,linux/arm64 push: false - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - build-dev: - name: "Dev-build" - permissions: - security-events: read - packages: write - actions: read - contents: read + todo-management: + name: "TODO Issue Management" runs-on: ubuntu-24.04 - if: github.ref_name == 'dev' - needs: [test-building, Anchore, CodeQL] + needs: validation + permissions: + contents: write + issues: write steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - - name: Setup python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Github Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ github.token }} + - name: Checkout repository + uses: actions/checkout@v4 - - name: Generate Docker tags - uses: docker/metadata-action@v5 - id: metadata + - name: Process TODOs + uses: alstr/todo-to-issue-action@v5 with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,enable=true,priority=200,prefix=,suffix=,value=nightly - flavor: | - latest=false + INSERT_ISSUE_URLS: "true" - - name: Build and Push Docker Images - uses: docker/build-push-action@v6 - with: - context: . - file: docker/Dockerfile-dev - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + - name: Commit changes + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add -A + if [[ $(git status --porcelain) ]]; then + git commit -m "Automatically process TODOs [skip ci]" + git push + fi - build-pre-release: - name: "Pre-Release-build" + deployment: + name: "Docker Deployment" + runs-on: ubuntu-24.04 + needs: [security-analysis, container-scanning, build-test] permissions: - security-events: read packages: write - actions: read contents: read - runs-on: ubuntu-24.04 - if: "github.event.release.prerelease" - needs: [test-building, Anchore, CodeQL] + strategy: + matrix: + type: [dev, pre-release] steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Checkout repository + uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Github Container Registry + - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} - password: ${{ github.token }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Generate Docker tags + - name: Determine tags + id: tags uses: docker/metadata-action@v5 - id: metadata with: images: ghcr.io/${{ github.repository }} tags: | - type=raw,enable=true,priority=200,prefix=,suffix=,value=pre - flavor: | - latest=false + type=raw,enable=${{ matrix.type == 'dev' && github.ref_name == 'dev' || matrix.type == 'pre-release' && github.event.release.prerelease }},value=${{ matrix.type == 'dev' && 'nightly' || 'pre' }} - - name: Build and Push Docker Images + - name: Build and push uses: docker/build-push-action@v6 with: context: . file: docker/Dockerfile-dev platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.tags.outputs.tags }} + labels: ${{ steps.tags.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max From 749c28653a34d8ab8dabdb2670bbc0f9f0b8e241 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 16 Feb 2025 16:02:57 +0100 Subject: [PATCH 132/369] Feat: Test new workflow --- .github/workflows/validation.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index dfa18edf..d4349e9f 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -155,7 +155,11 @@ jobs: contents: read strategy: matrix: - type: [dev, pre-release] + type: [dev, pre-release, release] + if: > + (matrix.type == 'dev' && github.ref_name == 'dev') + || (matrix.type == 'pre-release' && github.event_name == 'release' && github.event.release.prerelease) + || (matrix.type == 'release' && github.event_name == 'release' && !github.event.release.prerelease) steps: - name: Checkout repository uses: actions/checkout@v4 @@ -176,7 +180,7 @@ jobs: with: images: ghcr.io/${{ github.repository }} tags: | - type=raw,enable=${{ matrix.type == 'dev' && github.ref_name == 'dev' || matrix.type == 'pre-release' && github.event.release.prerelease }},value=${{ matrix.type == 'dev' && 'nightly' || 'pre' }} + type=raw,enable=${{ matrix.type == 'dev' && github.ref_name == 'dev' || matrix.type == 'pre-release' && github.event_name == 'release' && github.event.release.prerelease || matrix.type == 'release' && github.event_name == 'release' && !github.event.release.prerelease }},value=${{ matrix.type == 'dev' && 'nightly' || matrix.type == 'pre-release' && 'pre' || matrix.type == 'release' && 'latest' }} - name: Build and push uses: docker/build-push-action@v6 From dec86d312c5c146d2c3dbba46dae41de7ecc54a3 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 16 Feb 2025 16:06:19 +0100 Subject: [PATCH 133/369] Fix: test new workflow --- .github/workflows/validation.yaml | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml index d4349e9f..9a9ec937 100644 --- a/.github/workflows/validation.yaml +++ b/.github/workflows/validation.yaml @@ -155,19 +155,33 @@ jobs: contents: read strategy: matrix: - type: [dev, pre-release, release] - if: > - (matrix.type == 'dev' && github.ref_name == 'dev') - || (matrix.type == 'pre-release' && github.event_name == 'release' && github.event.release.prerelease) - || (matrix.type == 'release' && github.event_name == 'release' && !github.event.release.prerelease) + include: + - type: dev + # Only enable when pushing to the dev branch + enabled: ${{ github.ref_name == 'dev' }} + - type: pre-release + # Only enable when a release event is published and it's a prerelease + enabled: ${{ github.event_name == 'release' && github.event.release.prerelease }} + - type: release + # Only enable when a release event is published and it's NOT a prerelease + enabled: ${{ github.event_name == 'release' && !github.event.release.prerelease }} steps: + - name: Exit early if deployment is not enabled + if: ${{ !matrix.enabled }} + run: | + echo "Skipping deployment for matrix type '${{ matrix.type }}' because conditions are not met." + exit 0 + - name: Checkout repository + if: ${{ matrix.enabled }} uses: actions/checkout@v4 - name: Set up Docker Buildx + if: ${{ matrix.enabled }} uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry + if: ${{ matrix.enabled }} uses: docker/login-action@v3 with: registry: ghcr.io @@ -175,14 +189,16 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Determine tags + if: ${{ matrix.enabled }} id: tags uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository }} tags: | - type=raw,enable=${{ matrix.type == 'dev' && github.ref_name == 'dev' || matrix.type == 'pre-release' && github.event_name == 'release' && github.event.release.prerelease || matrix.type == 'release' && github.event_name == 'release' && !github.event.release.prerelease }},value=${{ matrix.type == 'dev' && 'nightly' || matrix.type == 'pre-release' && 'pre' || matrix.type == 'release' && 'latest' }} + type=raw,value=${{ matrix.type == 'dev' && 'nightly' || matrix.type == 'pre-release' && 'pre' || matrix.type == 'release' && 'latest' }} - name: Build and push + if: ${{ matrix.enabled }} uses: docker/build-push-action@v6 with: context: . From b0b5b161bcfe8b30ef95ce806d7e2778eb02ccb4 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 23 Feb 2025 09:06:51 +0100 Subject: [PATCH 134/369] Feat: Switch to bun / ElysiaJS --- .dockerignore | 151 - .github/DockStat-dark.png | Bin 82847 -> 0 bytes .github/DockStat.png | Bin 79885 -> 0 bytes .github/workflows/build-image.yaml | 63 - .github/workflows/cloc.yaml | 27 - .github/workflows/remove-stale.yaml | 17 - .github/workflows/validation.yaml | 211 - .gitignore | 172 +- .npmrc | 1 - .nvmrc | 1 - CREDITS.md | 106 - LICENSE | 28 - README.md | 72 +- TODO.md | 18 - __tests__/auth.spec.ts | 38 - __tests__/config.spec.ts | 49 - __tests__/database.spec.ts | 35 - __tests__/frontend.spec.ts | 123 - __tests__/getters.spec.ts | 99 - __tests__/util/previousResponse.ts | 23 - bun.lock | 119 + docker/Dockerfile-base | 76 - docker/Dockerfile-dev | 76 - docker/docker-compose.dev.yaml | 40 - docker/docker-compose.yaml | 82 - environment.d.ts | 12 - eslint.config.mjs | 12 - nodemon.json | 14 - package-lock.json | 13317 ---------------- package.json | 124 +- src/config/db.ts | 23 - src/config/hostsystem.ts | 93 - src/config/initFiles.ts | 42 - src/config/stacks.ts | 260 - src/config/swagger.yaml | 2084 --- src/config/swaggerConfig.ts | 10 - src/config/swaggerTheme.ts | 6 - src/config/variables.ts | 26 - src/controllers/auth.ts | 64 - src/controllers/containerController.ts | 54 - src/controllers/databaseMigration.ts | 20 - src/controllers/fetchData.ts | 76 - src/controllers/frontendConfiguration.ts | 297 - src/controllers/highAvailability.ts | 285 - src/controllers/notificationController.ts | 60 - src/controllers/proxy.ts | 15 - src/controllers/scheduler.ts | 91 - src/core/database/repository.ts | 74 + src/core/docker/host-manager.ts | 38 + src/core/plugins/loader.ts | 21 + src/core/plugins/plugin-manager.ts | 35 + src/core/utils/logger.ts | 72 + src/data/frontendConfiguration.json | 1 - src/data/template.json | 3 - src/data/usePassword.txt | 1 - src/handlers/api.ts | 142 - src/handlers/auth.ts | 72 - src/handlers/conf.ts | 98 - src/handlers/data.ts | 123 - src/handlers/frontend.ts | 138 - src/handlers/graph.ts | 82 - src/handlers/ha.ts | 70 - src/handlers/notification.ts | 76 - src/handlers/response.ts | 41 - src/handlers/stack.ts | 168 - src/index.ts | 45 + src/init.ts | 69 - src/middleware/authMiddleware.ts | 51 - src/middleware/checkLock.ts | 21 - src/middleware/rateLimiter.ts | 8 - src/misc/.tmux.sh | 1 - src/misc/createEnvDev.sh | 44 - src/misc/createEnvFile.sh | 44 - src/misc/credits.sh | 29 - .../dependencyGraphs/.dependency-cruiser.cjs | 359 - .../dependencyGraphs/createDependencyGraph.sh | 41 - src/misc/dependencyGraphs/mermaid-all.txt | 113 - src/misc/dependencyGraphs/mermaid-api.txt | 26 - src/misc/dependencyGraphs/mermaid-auth.txt | 19 - src/misc/dependencyGraphs/mermaid-conf.txt | 26 - src/misc/dependencyGraphs/mermaid-data.txt | 19 - .../dependencyGraphs/mermaid-frontend.txt | 19 - src/misc/dependencyGraphs/mermaid-graph.txt | 15 - src/misc/dependencyGraphs/mermaid-ha.txt | 31 - .../mermaid-notificationService.txt | 15 - src/misc/entrypoint.sh | 35 - src/misc/minifyDist.sh | 38 - src/misc/removeUnusedDeps.sh | 36 - src/plugins/example.plugin.ts | 11 + src/routes/auth/routes.ts | 18 - src/routes/container-logs.ts | 11 + src/routes/data/routes.ts | 20 - src/routes/docker.ts | 22 + src/routes/frontendController/routes.ts | 76 - src/routes/getter/routes.ts | 46 - src/routes/graphs/routes.ts | 20 - src/routes/highavailability/routes.ts | 27 - src/routes/logs.ts | 30 + src/routes/notifications/routes.ts | 20 - src/routes/setter/routes.ts | 20 - src/routes/stack/routes.ts | 35 - src/sample-variable.json | 24 - src/server.ts | 18 - src/typings/atomicWrite.ts | 6 - src/typings/dockerCompose.ts | 92 - src/typings/dockerConfig.ts | 35 - src/typings/dockerStackEnv.ts | 10 - src/typings/frontendConfig.ts | 12 - src/typings/ha.ts | 20 - src/typings/hostData.ts | 26 - src/typings/response.ts | 6 - src/typings/stackConfig.ts | 5 - src/typings/states.ts | 10 - src/typings/syncRequestBody.ts | 5 - src/typings/table.ts | 11 - src/typings/template.ts | 5 - src/utils/assets/api-icon.svg | 1 - src/utils/assets/container-icon.svg | 1 - src/utils/assets/server-icon.svg | 1 - src/utils/atomicWrite.ts | 35 - src/utils/connectionChecker.ts | 67 - src/utils/containerService.ts | 173 - src/utils/dockerClient.ts | 41 - src/utils/extractHostData.ts | 73 - src/utils/logger.ts | 79 - src/utils/notifications/_notify.ts | 51 - src/utils/notifications/_template.ts | 76 - src/utils/notifications/discord.ts | 56 - src/utils/notifications/email.ts | 53 - src/utils/notifications/pushbullet.ts | 59 - src/utils/notifications/pushover.ts | 57 - src/utils/notifications/slack.ts | 56 - src/utils/notifications/telegram.ts | 56 - src/utils/notifications/whatsapp.ts | 58 - src/utils/rateLimitFS.ts | 36 - src/utils/startServer.ts | 16 - src/utils/swaggerDocs.ts | 12 - src/utils/webSocket.ts | 113 - tsconfig.json | 118 +- 139 files changed, 625 insertions(+), 22275 deletions(-) delete mode 100644 .dockerignore delete mode 100644 .github/DockStat-dark.png delete mode 100644 .github/DockStat.png delete mode 100644 .github/workflows/build-image.yaml delete mode 100644 .github/workflows/cloc.yaml delete mode 100644 .github/workflows/remove-stale.yaml delete mode 100644 .github/workflows/validation.yaml delete mode 100644 .npmrc delete mode 100644 .nvmrc delete mode 100644 CREDITS.md delete mode 100644 LICENSE delete mode 100644 TODO.md delete mode 100644 __tests__/auth.spec.ts delete mode 100644 __tests__/config.spec.ts delete mode 100644 __tests__/database.spec.ts delete mode 100644 __tests__/frontend.spec.ts delete mode 100644 __tests__/getters.spec.ts delete mode 100644 __tests__/util/previousResponse.ts create mode 100644 bun.lock delete mode 100644 docker/Dockerfile-base delete mode 100644 docker/Dockerfile-dev delete mode 100644 docker/docker-compose.dev.yaml delete mode 100644 docker/docker-compose.yaml delete mode 100644 environment.d.ts delete mode 100644 eslint.config.mjs delete mode 100644 nodemon.json delete mode 100644 package-lock.json delete mode 100644 src/config/db.ts delete mode 100644 src/config/hostsystem.ts delete mode 100644 src/config/initFiles.ts delete mode 100644 src/config/stacks.ts delete mode 100644 src/config/swagger.yaml delete mode 100644 src/config/swaggerConfig.ts delete mode 100644 src/config/swaggerTheme.ts delete mode 100644 src/config/variables.ts delete mode 100644 src/controllers/auth.ts delete mode 100644 src/controllers/containerController.ts delete mode 100644 src/controllers/databaseMigration.ts delete mode 100644 src/controllers/fetchData.ts delete mode 100644 src/controllers/frontendConfiguration.ts delete mode 100644 src/controllers/highAvailability.ts delete mode 100644 src/controllers/notificationController.ts delete mode 100644 src/controllers/proxy.ts delete mode 100644 src/controllers/scheduler.ts create mode 100644 src/core/database/repository.ts create mode 100644 src/core/docker/host-manager.ts create mode 100644 src/core/plugins/loader.ts create mode 100644 src/core/plugins/plugin-manager.ts create mode 100644 src/core/utils/logger.ts delete mode 100644 src/data/frontendConfiguration.json delete mode 100644 src/data/template.json delete mode 100644 src/data/usePassword.txt delete mode 100644 src/handlers/api.ts delete mode 100644 src/handlers/auth.ts delete mode 100644 src/handlers/conf.ts delete mode 100644 src/handlers/data.ts delete mode 100644 src/handlers/frontend.ts delete mode 100644 src/handlers/graph.ts delete mode 100644 src/handlers/ha.ts delete mode 100644 src/handlers/notification.ts delete mode 100644 src/handlers/response.ts delete mode 100644 src/handlers/stack.ts create mode 100644 src/index.ts delete mode 100644 src/init.ts delete mode 100644 src/middleware/authMiddleware.ts delete mode 100644 src/middleware/checkLock.ts delete mode 100644 src/middleware/rateLimiter.ts delete mode 100644 src/misc/.tmux.sh delete mode 100755 src/misc/createEnvDev.sh delete mode 100755 src/misc/createEnvFile.sh delete mode 100755 src/misc/credits.sh delete mode 100644 src/misc/dependencyGraphs/.dependency-cruiser.cjs delete mode 100755 src/misc/dependencyGraphs/createDependencyGraph.sh delete mode 100644 src/misc/dependencyGraphs/mermaid-all.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-api.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-auth.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-conf.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-data.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-frontend.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-graph.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-ha.txt delete mode 100644 src/misc/dependencyGraphs/mermaid-notificationService.txt delete mode 100755 src/misc/entrypoint.sh delete mode 100755 src/misc/minifyDist.sh delete mode 100755 src/misc/removeUnusedDeps.sh create mode 100644 src/plugins/example.plugin.ts delete mode 100644 src/routes/auth/routes.ts create mode 100644 src/routes/container-logs.ts delete mode 100644 src/routes/data/routes.ts create mode 100644 src/routes/docker.ts delete mode 100644 src/routes/frontendController/routes.ts delete mode 100644 src/routes/getter/routes.ts delete mode 100644 src/routes/graphs/routes.ts delete mode 100644 src/routes/highavailability/routes.ts create mode 100644 src/routes/logs.ts delete mode 100644 src/routes/notifications/routes.ts delete mode 100644 src/routes/setter/routes.ts delete mode 100644 src/routes/stack/routes.ts delete mode 100644 src/sample-variable.json delete mode 100644 src/server.ts delete mode 100644 src/typings/atomicWrite.ts delete mode 100644 src/typings/dockerCompose.ts delete mode 100644 src/typings/dockerConfig.ts delete mode 100644 src/typings/dockerStackEnv.ts delete mode 100644 src/typings/frontendConfig.ts delete mode 100644 src/typings/ha.ts delete mode 100644 src/typings/hostData.ts delete mode 100644 src/typings/response.ts delete mode 100644 src/typings/stackConfig.ts delete mode 100644 src/typings/states.ts delete mode 100644 src/typings/syncRequestBody.ts delete mode 100644 src/typings/table.ts delete mode 100644 src/typings/template.ts delete mode 100644 src/utils/assets/api-icon.svg delete mode 100644 src/utils/assets/container-icon.svg delete mode 100644 src/utils/assets/server-icon.svg delete mode 100644 src/utils/atomicWrite.ts delete mode 100644 src/utils/connectionChecker.ts delete mode 100644 src/utils/containerService.ts delete mode 100644 src/utils/dockerClient.ts delete mode 100644 src/utils/extractHostData.ts delete mode 100644 src/utils/logger.ts delete mode 100644 src/utils/notifications/_notify.ts delete mode 100644 src/utils/notifications/_template.ts delete mode 100644 src/utils/notifications/discord.ts delete mode 100644 src/utils/notifications/email.ts delete mode 100644 src/utils/notifications/pushbullet.ts delete mode 100644 src/utils/notifications/pushover.ts delete mode 100644 src/utils/notifications/slack.ts delete mode 100644 src/utils/notifications/telegram.ts delete mode 100644 src/utils/notifications/whatsapp.ts delete mode 100644 src/utils/rateLimitFS.ts delete mode 100644 src/utils/startServer.ts delete mode 100644 src/utils/swaggerDocs.ts delete mode 100644 src/utils/webSocket.ts diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 6381947a..00000000 --- a/.dockerignore +++ /dev/null @@ -1,151 +0,0 @@ -# custom paths: -src/data/* -.tmp -docker/master -docker/slave -.test* -# Created by https://www.toptal.com/developers/gitignore/api/node -### Node ### -*-audit.json -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -### Node Patch ### -# Serverless Webpack directories -.webpack/ - -# Optional stylelint cache - -# SvelteKit build / generate output -.svelte-kit -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ diff --git a/.github/DockStat-dark.png b/.github/DockStat-dark.png deleted file mode 100644 index 00ac779a6dcb303ce5c690aa079dedc175e5c8f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82847 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

TLgl9u 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 AX#fBK diff --git a/.github/DockStat.png b/.github/DockStat.png deleted file mode 100644 index d375bd49107c79a960488d6062276a72cf6bd512..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79885 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⪼EggaS+_T%ej z-K7Mn&DAclEk>mFh!VsaOdx>a5eLlWQ*DjgwF^P$fIiPJscs^SXJX75WN@xgz2z5D zv=yWr>P?lfqOC4)ZpxM42(~RaM+Y{aMILn*+Y?#I&qVIzZ8e%1Wi5C)L z3K&>Gz!(Zi?6b^7fd%3HYllbIqrp&`v6WRMn&K4M(Ids^%c*^&VZBgnG|$DiljZq+ zXF06FZ68F{?{-sE^u`grX^*4V@E)J{qmZi$;qWIb9BnRI#+=cX6)dG+%^MAMZZyd3 zz$P~wu6KMOL9uM%S(y&nu&(+8i#0n4atG0*Bl5}v!Ki3HsokgGo`xT&Nzn>ns`O@{ zhP8W3$pf5vk7xK}(LA+!7EGp;$gBNU5@jcfEQKCLaa4$GUO>)inzIT`jmHbW_d?9-JD;xIVE`hb?~hw$SZ*q z-|s#fs0V3mZ9Gg3;6{5>YKb!?>!zyH8*^F6A*>H&U(_u%V|9Ya9QoDKj#O*Un7`dlE|A?%MtvML zwUVm(qft7e2+_Qew!scKq*OU*^jgRTaBLrm0DRDa@TjRJZZ2n8*KH@+{Zdk!bF7L$ZMLXbC}`*hTg}+b0vV(>jXE-S+IjNfV1$*J8~G)Q zBF;Q1jr#Py9wwxVZDfI`QKZWUTx-Jl53xX22gjxMx_6F$6L$q)LHFa8j}S4gcoclZ zHo+lNv@kYqFL_ZBA}Mt(OyBy@1k23Qihu1p<#S z8B-ArT>Bt(blG0hnnc0)I^J|Jfv2E-jq|w(NIjF*bI_)M(#Ks`Clb;l;}leCTFFWZ}XYQT?dU}8Y=QOT35MLWa5O)bz-$B%wdzqNW>KxpAM@-39} zeMX^iTlu~1GsXr)tvu1OWQmrpB=E8uSn6IoMkF>0yDNX!MD_fPnLNM zfLD8C*@xSUUI`p_;NL@wFkIF4!mjbA?4}oUdug_#^Y(z~02%num)a)Y(5FwAG;9YM zUvaBC5O3RZv&YixHC!wk(E^~O1vmHHE_2QVc@$D#*@1ZCSndcDg6K$I3uEmwK{BjN z7kTW7sNe2N?|s0R1KFUp%evVX-7{qDnkZwq``%|E`nz0XB*=#=Z{WNT%~`dw3%E+k z6WP&2a)+a-`b~gmgp30Nb6CzI|tAnAN=6dR5OqarY&o z&z1*bnd%B1tq!J(lDNPfB>g2I;V8PbdjaQyvPi0efDeVZ`|#au9|x0PmvA zUBOvo)t0CMc|~VS@OmiJT%CT@vRJpcTK|Q7As||BEHf_bDwu+!I&xEe@1ce@kiXw{ zSF#tu>*ibCM4Ea&1Ux-3>|mO{ZPgUdqK;i};+5%b{;b4s2T!YlDOe^A)x7dS7t!&@ zm=CE*GE9%-J&pcgXWlp>dKZM?2Wg*tJDKs{ulHaDvXJUTQHV{n#L{ul&ScF7Q#7LT zmPh?-vv6vFK2=E9LVs@E0_S*!J9EXj4|^5YE1Kf4>aO9DZexi@eZeA+^E zeba$U{SER+d1jL!2sy_0&E3X{q6z-5#V=AuEvT-6xB@tfb~RO>jsKL*5gUlgZ~b%- zazEwoU)(j}ES5-2FlrF{{ zOO@NE*8uo89Nz&T_SeQpI>{}f^+YJhEYbZMHyUUWf}&I4n$qjuT`gX^8|vFxPYh?^ zpz|MHWU%h|g>!^WGFjtV@KCnwp4K(1Paa{11y6}4sJ?PTR)dpnc6S<5+SdefALw70 zRqAQ{VW%p}U;Gz$2@qX$St^(3bik1KTvLM1&u=oiJ$K(VfeV&cv{!VY-_RH^Z=~yd zCD}&MeJkx7uqUPiGz>R)MGCcOMvinD`%ZtUiC18GZa9byC^IrbyC42ofa(ppgSR73 z+&lbh)t-5pTP(rq=3(lloB=rM7Dt8K#1++1*BZhSL(xTQ0v(v*6GE(wKu=llrMTZy4H zA(vZnadI&j6G!$F_Vp8;g6*lzrb3b6Gu9~d;B!I2WH7vJcHr_;YAOiWmGLo9AhekH z^&VK(=5S3v6TIG|6dFO0-x{+)Y|5)AQe*74Bpdl?&;B-oMfuhw;Sj=WW@UzU zWCZQ*1J9HYeG80I8K#qJ$d)mvOwpHtKfq2U|9We&)XcQ~vgZ%*E4+C-nK?0<46&!= zOKqxZTpRz{fmrlJW#w+?VDE46yb)f?oN)CM71;k+hi=XGW6MN!x;#87VZqiXYZivV zB0!>DPV%#9NU8KDEIPGzx3HDRZmA5mRXGv#wjd9u!0N zEQYJKi0zh8*$gwlmJSdYQoV89=)EJNzvQF#W;Zw8m~F|&f%FBL%|c(%%PVf_rr){Y zgLAz8t;}8i#+Cmo`hS!I&lIRERyWv)Kx ztslWEkvz+qD-8s*OJS?nTLAuJXp5d98GQCZwoPf#!7rA#ogDCUpqaCWjNM3E^;7ai zfl~pIgo~{78a|Y&6u?@v%`F(2Uj}zb2?~C6KC%)4cY3j=3)W@U1b#v#Ok?D; zlI*pOoP>^DfGP|?J3JrG_5wcckRFOpR>FB~X%d6KT%l|=c-0@L^F5I84WKcXR*R~t z?#K$iX4Mu5>Z(26YEeV^acA2*Io50jKAF-#0aA5)fPUTpzd@zP=AAIm(1Vp#|6AFI zA)({;Jqv}ghPw%F{T?j?-nCaFU`=_|5;Kz%NN4rMK?`_%c7(EhDRAeqq_|nE`O{PzvWQ3U0i8i?LBY`$JxIR-B zhy=Z!{?Z!(8rGm6T$1cN-m_g1cn5%;V6%S_#$cv9e*iU3l<;(-reXHBPx(*6zEl2y z``aP51aQf~dpNK5s?sR8R-TJ${uItbECXLdDbDTN90B3>4jlWq%Cv>X2r&Gi0dlUq zS_0!~Nl2!zoRf_f$`0EMXIbqAWm*!js8P>gJuClJgg4SsfyJTi6!5LPsMcCVoX?X3 zWlf(ZeHiTF1T%FYQ%ZtHYWi;_vy%CDjLS2*cp&xJF^FDk(RiHOyA#s|Y&^O@<=+%f z6ta2FqXs#6BTbWO3%8y)t&fi*$Z~7oVZWJ^zVrGo05(fQ>!a-#NXvC!ep`|k8Ng-M zs;zKm5gteMoQCZ6No6P1(F=1}1}H9)(_$PtssJyHf?4LpC(hn0nIi$E?WTszsvAMM zboXV=w(wD!#S#-L0X{`;3z@GVa2(a!1nT8zLxr67BPr$cQ9Ex|>LGW-Zz+&IGZ3=0;inGANX5xE>p z0TnJ!S&d)MwkK{l{Bb#_rAH5VD93MS1{-%{Yjpz=?@ z``b;Ya7K#<6M!`NFCIZ(8{lE?6zURnLj8+=&<26yoBK#G=2BfSQ9}iEBT@MbC^2xM z!dKL(DpUE^iUCn$i%Lfv0sCS&F$BVvT9m6R{enDvClu=7!SxPJ3)u3>e?>?!qx#9o zH=!+{Sf^ThbYq5;f}4PnV&v?}4XmfyGgok_ecF{lUNi}0EJ?-pwii#i)^G`C#{{9{ zyf!afT7T>PkD{H_mAxD(HX9WXG79+O#BG2P(`T9FQ3GHEM26HB!3X>$g{`ZVci$nu zYdmAL>@92EZCvlKMxc7fF#$>ajD_8(O(}R~{j@&Ij2usfD9Ys^e`w-RV0IJPsbaD{x;z?_s$Z5%lM7-Jd0 zjRA7-ga2cIFpSgVE~Jb*wE{N2HU7v_CbLDF2YqE(F6w2uuH=E{iL02u*t z1#(978mCM?@ub@JnO@UlZpWMAge)wm0??!W+r#D1*0*sWO!k*RmI#$Mr_T%w5-5X& zD~k&TfN+wjbw@97;kxD7LOFt^ZD<$|W zV}3otoU}ByBS&398)sz&tSmHW0|XPrQZMWj+SgGsaQ@$MXND1b#bSxJ%C@ zt$$>)<4cp9g~ffg;9k2~Lo^TfULK#xe+4D9%hzRUl}x^hT+Q0hfE8}T|%gBeED3sO{QgPHG-h)fJIg=F7(V*o}Kq7h2P*kO)Z~6 zthB{o0uJFF0G|1P#m3gOJbQvfDt0UVcmYoR)k)*?=2LEM~LdeK>%&f$piBYtjM^ zQ@_aN9NhF@gR6C;q*@V|FnU7WngNEkj?C)Zd){FqoA1rj434y|%$6tLjH^Y>!EEOK zmdmQuqf?c>_iy!^^W&}D;pfVIc`9LGs8J z6Gp6#J(?of%tjT2uSio1L-DmrsXadV#)Yaq7tSV$?hWx-JMx=$X z7b)Q0#&y8XY;{n<$Jd0uURVmA(mJMjnYFY%0>d_b3lr5%9Qa&<(>2^pimrIQkyRDG z0^)zmkq?VjUGg)Uj_iYB8kWtkXFVl{<~qd2Tktr#mXj^_KO-rzq#+XSu}mXDUmukR z&+=_u*p3JfG#_{<9nwlUJtOpXqM!*Hy^sE1fDw?$cawcy1q7k0)KIi@E-OxvJ&ySS z*qt`ghR(c3vg>p8zV;-k2?c{ShEQw!{Dd0?vgGlSlSLj1w+VJ%F%s z;2`)c1JQm^KFp}f?k(WAe&}J?uzvzRg|z@0GmAvNxmb7Ub7Z0aiSYkUWWHu-J6{a? zI(`A|%y-mWH6WO8@BGXmxGlAF%?s0zj%+|papxmTTe1zHtqLe*^kip*j-B`9#GfrO zFdfDtSX7O0+b&Bz<@qdOf~NUQ5wKV2;tDs+oRJwnN7(gcd8*w|7Y~=kxpLdKi>3T4 zVHhg>W!~(x)gyHKOj0oEHRuNxUIoX-Zz}m6wWxk{(>r}!Bz62oFwEwHf6h-?mkjW@ zpoDZFdR?LdHvb|oCPg}f;IU|$2Xe=q#&$VP<2^t!X|FRiyW@GUqlGsmb$NdBEUg7H zpM0%ndCeMgA6YRP6n5?RhxIq`6WS^XPd?SHnPVEj4I=iqTfZKuxTPodp3SQtnu&NS zd`QdqJqo#bt|@Y27~7>jgQ{n5O74Lky;>m$Zj+P>wgwpep7E;K-Lkh{DJPAZIZJHK z?@m27{PpN5%}V~#AsTr03LO%@AM#b2*(*fBh|@xD@3>Uy=G{sPv=qAhXziVgS?3<0U6)kUToHJ>1!H|fhrgym6rMK6@8teZodInOGE>7tPfWWV*k1H z0JL2uu8AK5IRIGCSMF`PF?v+T7H%`q=p6T3_Yt7GUWMu(PM0EW zsdFp($BUsE7MCeURC-CaV|5pTFB|}xps2}NoR4)en7jJum8Z&ccF0fI4t|7J43^rX z)NobXv?_EaVRZFO<3{A~(;6_ZA2BwvzBU16r9WbwB-YLHaVCCC**n?1v4UsRQ^((w z&79Y`F!6KTMt00*f1+JM*xuH(1Tm-nPY60>C*#MVx81?>^?-Ssty`d;BzUNsZ1Oav z_}&4@Q?NpT#xOPOKFUx&T$!2Jw)jw|b!g|sbGU5l%J0GAkqZ?EKp&+`e9T@UPcq~I zjoMC_k4uM{40S4Jv__I5 zhPDoD4%6}O?AKX}t0d#})o^}-qz~fwRT?#|-|4G;#(=rbOa_s>@B@XbY3x2lWHfIv zVoC#9+U1i_VHpxjJXQy2CQfUIDqx>0aDhM|c0+zVtk%FEhEuQ|P4bw`o>F@zp_J4W zhiK=@FawfPl{SAssdR;W>ySg_X3ngmx7R!uSKOma0nH%T2>S5mc9;?{FNqiAs`B{y zP#3Gm%WSjNvR$)gOv$xpb+Mh;)!JAN`TD^5)H`kVYxi(|Uur)-4$VLu$#Tt)s7RZJ z4*=2mi7~;T`P%QVns9zyoBj`@@BDtI1|Ik953taSVZeGA4+`cmlj(z%a?iTb?XDMp` z%cRE=ODSNI*YMCI2zjlCDXP2&h56QX*HWEOaKCLqcQoj%s)tQx{#q-@Bi%1`AV0m; zFKYdy+>M*I!u;`?oSP`hE$@cYwxvEhd)v~S7&cEy`WVOT!!=w&6&BD|y3^rN(NhiL z4(h|lTCbdbkQesSeXzP-Eb`&8(>lHNRDAv{5R&Bimn+m)9$atZ;NYWGI4@Ek{^h`d z&ZG8`Sz{*(4*gvJ4%Cg?LQ+7QR&eZlr_l28m(cO3??)xdobqQ)VlPihixnF+CjCl$ zap)Hh1R`K=ct!6v2V47{9kJ0|Wt);C3ms}C!q0K3NYb0Z70bM$ZF*~&4ZF9W(`d4@ zb&8~Y7o4*^5+Kvi+B@%p%xZGhkOfKm%i;Ss&VKH2#{#rtKggC4XoFMf`JTsVS^mEo z=T;QJj>S+`t?=WEkzo50|1xST2#=Ba#W4H}aY(IURJ}OQk3C!2R|*rwY>T893#dCR8d1eeO4JgTN@4$~_=8FfX z{U{7P)~G#D@wkf}ihb?cu;CSyIeV-BjbKgngSp<6qf%ngz$EqCZjnc@7O;eXg<*{9 z01<&Hd!E{?XNs^pM0Gg%W!`J;)8W+LRl>Ru_r5rUeKN!+ve}T+;2_&)y7t!!>6BV$ zzC_;18(!vAKakH^$m_Ds1Dz@Ss&yke3=n`9K0f}9braaP{q2zhf~N=>z>mG{j&s;j zveWvnb#BETT!p!G>rc{Aids}g5 zSte^u$kHg1WiZ(q8T*i(-}zE*@B8=r=llG{d}le&^PJ~A&pDsZbI!crKoS?*o9)tI z8s-zxHt(Vy<<#Y+sr`|+A)Vi!bOqa+V>1zUNRhqNDf6u9RoCq{R(O(-c*Gls+*j-G zJeSSQ$3KVdGR4J5w+D!W96zb{A}~pJ^5$s5tNX}fE5VK`Q*_n~6Rq1ySE&Ol7DQkh z<-|RqoSAMvc`-%%_%3-egz%K&s(s0M%&m53z2Y0wZpY2XitpOaKSHexdn20|G>8x9 z4W4Tj+6{ad$GYkp1ihG_e+*8ug44KV&D$7@Of=hNN=k}yIhOyz>= zvfD|tg*cf6lhTKgQ6|ro^MwP&_|or0sn`bxTZ5aT#?J@1YSa~Kx-*kEiAVglO4e9m zXgx!i65z&WWX-jua^VlR87a=wp=S&KROMb70-_7t#g`IVZ)*lu+V05o@m$$b>CC9h zuc)=&6>0$V=Z29($GtVeJOzszG}hgE5*1E5nq0{Lxh5lO1H~S07?VBDZ&3;8aqnoF ztiil^>C$lVWb90dhfIZUeATXvq%gVh5MZ9}iW3E#c;d^!rqopyj(B9X>3|upwpm|L z{>0@+?n6IbDIKDVqIjRx_9&7RdJ`wR95Rgrjs_0>lkrxMvekkcPF2O$xRGVJ=$UA9 zq2JEVv1gU`m|Q)_2)`e`+XT(d@e{d!mL8OCc&3VNyVJ-_;?Fj_?^>gqq8J-{{~TJc z*K@b{hmv)`hM*zGtHl*5nzEJ!#=G-+#}O1Y5>x*EFe$>Ehu%`bn!6*8E{@FmtI%z_ zSI(rKE+*F%-c0I$p^NUMMmfX9XMFOgi2WH~8SBdT<<;0}Cy71J=i)OC)=x;kyJk$& z$|kH&$!)hg8jxVdFZCKE1eLb}p}mK#)2*D)NTyvO zuw5v^S%6gl=E|aJwSqT1;{KTR1~n94*WDe|I^j{j{?>N9Ko34VrC5&nbo2MJFMF^> z|1_uOcrFbC%H@NT=nrciEa&hAJ37dB&syiVBKH#SEBAx1tSj*{Xx@Y%NUZ^&z>#Ks2o0Xbb)#O^APnN?+2TGY>iUKx?^Y6i~O>F$(T=u`4 zcKuZ0DZlceKT5poL|Du3V4fc1k0k+k8(`;`u)o4bp#~#=zvw=5;NVG}dxuA%?rlRQ zseke{&r0#3xLoj*uQ+$!d*%>m?v>HpE72ZweWJeaCK2ZEYG0nc$n>+F$ z-6GUy{e02i8CZL5x}Mcv`e<~toT?4OZcOAu`GRVK(#I8l*_2*JXsmNk=tbQ823kob zXz;uVFb+^b-sJ0_jA~P#@`TjA<0p>nA2@C(&!YBEkoxf!XPO2}+<|q_YM#k2{nN2< zdatBFQm$r=eoRk>n1y49g)npaVbClce{1BWOG{kq%`l#&QvDIw8JQQv)&*fs)T=86 z`D}a+@hapT=8SktWZJyWOpE_$dwMMi12YK8KM_#83s>B?t|Fquh{59A+$68oJ5j)t zpKOb+>!e2TqF-!@Pg`G}px&A;$|zF6)6Z`Xt8%X$jn{W9^i;3m7#Nw2P=4k-E{z(E zsnQ9noVV(zldjrTt9s5`K_ZVBCz5gzwLE-GvEG5ANVGM9jMQ`_Yf!C>Ti3+Hd!Pq*?RBi%dPcL80`q@}I53?x`&QQb9I`h{;}yieH) zQciEDzNPpUN*|9Qs_~|y_*HrxTlJJ68^mSd<8Isx%(DDF2rFC)_ZVN;_gg)koD}Mq zY|v2`*1Y#ZTFrKLp3@Fkv<{BW2;1t zIG#RW=#NtT-0$mtc6)!dm}Ph)^6zDl;@xNG z`S*E|-Ye?zTt+PQaMt8kq@BGA@)xSN!?YX9@<2i-%$0 z$a_h_8ef4*8tJ`t=(%e`Ke4CVsMg0SMuoONF>ml$zHGN0&PrsPu)ug4 zmr-;UA2z0fu5=>K*bN^gz09KKglFRHbt!ucpaY!tM!9ka|`)*=XN;rW>PV zW@5TFU*$(fhOf5d@3IA=SRH2bOuVDZ+`wVU-oQ(+1Y@DZi7W71LgTz_Oa0+=$Zywo zY0TqKWVU-x*3UZ>UO|zVZFx`LuQyTTDjta^6A@`AUG-E(pEaaEg$ zMd@EJ#}5164IS;|BXY(theYkQE(zhNRM9j3!7A$=4HAE`7vC#}`S>fOKfb&F)2198 zLT&s6!9hm2I2am3nvPS;+XLA#gb@L$Xns`l8)H5>WEjZ#Y;bt## zI=aZU>x;Q9Q*2|hyhfNKO%?XHQ4hdji@}c&SMJnY||sUxwajdJm?l2Fy#lvvVbiHKQhkR6> zjTRDb$%zs7U;LZ&lP26QyfMcz9{F71U+Xx`xZtau9?#<_+Nf;IB)m(kJTzU<#U<9? z$kYhiP|nNQkH14%LDGKC0%ihRnCrB8Y}anWqrFg73y1>b4wL|I>M2C) z+V!ns+=e;%2NKflUWe~GQQyMi&mJ2zz-@eGag7pvSNqj>HkRJXi7#!t&EeD?@co>9 zSo{IE5en|QdIp&L+aFcw2_URjY@hk?F48M@6mO^ZP+Et5mNkC+CC+>le|B)g)z>(a zW!G(~_;CI!dFj4~rU94dTP$QIIH|G^x-Yd=CIcmV#GO8S>7!to-ksQdK;x@(Wh8ST zSXKZ@%|bHag-FN&1q#bG_*`7@iO2jARY8xl@{_qB{ouJMHdN^&W8jsD)$6`SXP55U z>Dh&jZVd_4`%60gj6S(uzh4=TMX?QsGOzqq7s?8pl4>G`m{TX3c~7meio;y~CpHcU1qcPVK{l~@8!FCYUB!)E4J~9?oQI1H+sxlsOD%7-S=%PTfmHNtQ z*QpHWo#-Y^QpDnLhR$?nrVPQ@Q&lHt&e;vAsRz~)SPbTf|9}9Emq%IN zp(=%9%O`O0!Bl>IGfGGP1X|^&<~#jqgdxw?9tijLv5tWhx3JwKG`cUXXa`(Ay9;W- z$3!@_WXW95J-7o$9`u>V3#H&NSgG)S^8Xf{}XV;lbR)MDgF`a9l zN`aXErJ*Ckp?7d%N0RI!`0`3X?}=f_ETD&xeO$wm>3~wCjst*LUs^z%7-pj!cmT9M z!Z7}QA7Y_+SDA=`>U)mocnU5+Qv1+vI;pO30%?cYz#LgA3r=DmmrKgrAs9WBLhz4uv$BB%Q{*Dl-bSxf2=_j;!M%UU7QV!h); zR2eWjji96&%x4J*+aA~hY*T{?x5a*R`-=b1a*|y8`Yb?zQ5-mfc41~2AkG+s=WEQD z`=J9-eG#|K51d}p>s5Jjtd}zZ2U`RhVI-!(8ODEQGcjMPgpWx7)T3OwZ5Jd#UTFcH zsk>ECD@;#$vUzd%p~J>`04cW>E>(j_#uI;JZkyu32@+T|khXcXxd3Kf3hdy-J#Co; z!8<`19kA1go_XfY#epKl^kHg@r%n#0Wq=>tf?MlItF#``51~5!S^>)wc?cIl7Nfo& z@2s7geC=rG{PhQH)i;SB5P}&?>@!`7hp81eIgkmg$RR*zEQ-HhdmkSPLx!!618hD+ zk0K!2^X(5NE;r*)bSO@ef-#0*K~7MYuozU)SZNhALgEL01|by4R>dL#G81{L(C3%O z0mEMcH5K`whKm>l*kW)SY+u3Q|D*ml%7q6J5GOK5Tz=_qk`ij&B;qH0^9Cqgx4mR# zEFY0%Q0pM;N?g-7%C@3Bm(PQ(qLt7If)ywTg)023E*QM{lVh-MOpybCz#FswHPvrq z;rRhDYbPg84dYRM1Nr|hl`)`okxoLQb(&TdK~OhB3nb6^cj4eIOxh zS{q`N&cofRDPX&Ry=)3&yk)+~N_S0-#*X2|4{U}B*aPJo;Gy3)tHcU)J_W3SI}PEZ zw+6(l*z&cHqU#cjleC-Z0xNea6f_Jx3O%qW-q;lc%>GwV(Gl=1uy8(~dGQkD-|#^K zINN}rt=TD+(@+<*9pYOG|z4Mg_E2#lpg zLbUgrp>0(HGrrbTj;<`}wf}PvBfr*E{zt*$|Nrmn!2Dm0#Imgal2~cP6x|m2W&?er KU+70|um2a&GMD85 diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml deleted file mode 100644 index 9d43ff17..00000000 --- a/.github/workflows/build-image.yaml +++ /dev/null @@ -1,63 +0,0 @@ -name: "Build and Push Docker Image" - -on: - release: - types: [published] - -permissions: - packages: write - contents: read - -jobs: - build-release: - runs-on: ubuntu-24.04 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ github.token }} - - - name: Extract version and create tag - id: get-tag - run: | - # Remove 'v' prefix from release tag if present - VERSION="${GITHUB_REF#refs/tags/v}" - # Check if pre-release and append '-pre' - if ${{ github.event.release.prerelease }}; then - TAG="$VERSION-pre" - else - TAG="$VERSION" - fi - echo "tag=$TAG" >> $GITHUB_OUTPUT - - - name: Generate Docker metadata - uses: docker/metadata-action@v5 - id: metadata - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,value=${{ steps.get-tag.outputs.tag }} - type=raw,value=latest,enable=${{ !github.event.release.prerelease }} - - - name: Build and push - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - context: . - file: docker/Dockerfile-base - push: true - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/cloc.yaml b/.github/workflows/cloc.yaml deleted file mode 100644 index 0f4245f9..00000000 --- a/.github/workflows/cloc.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: "Count Lines of Code" - -permissions: - issues: write - pull-requests: write - -on: - pull_request: - branches: [main, dev] - -jobs: - cloc: - runs-on: ubuntu-24.04 - - steps: - - uses: actions/checkout@v4 - - - name: Count Lines of Code (cloc) - uses: djdefi/cloc-action@6 - with: - options: --md --report-file=cloc.md --exclude-dir=node_modules --exclude-lang=YAML,JSON --exclude-list-file=package-lock.json - - - name: Create comment from markdown file - uses: GrantBirki/comment@v2 - with: - file: cloc.md - issue-number: ${{ github.event.number }} diff --git a/.github/workflows/remove-stale.yaml b/.github/workflows/remove-stale.yaml deleted file mode 100644 index 47f9ae24..00000000 --- a/.github/workflows/remove-stale.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: "Close stale issues and PR" -on: - schedule: - - cron: "30 1 * * *" - -jobs: - remove-stale: - runs-on: ubuntu-24.04 - steps: - - uses: actions/stale@v9 - with: - stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." - stale-pr-message: "This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days." - close-issue-message: "This issue was closed because it has been stalled for 5 days with no activity." - days-before-stale: 30 - days-before-close: 5 - days-before-pr-close: -1 diff --git a/.github/workflows/validation.yaml b/.github/workflows/validation.yaml deleted file mode 100644 index 9a9ec937..00000000 --- a/.github/workflows/validation.yaml +++ /dev/null @@ -1,211 +0,0 @@ -name: "CI/CD Pipeline" - -on: - push: - release: - types: [published] - -jobs: - validation: - name: "Code Validation & Tests" - runs-on: ubuntu-24.04 - permissions: - actions: read - contents: read - packages: read - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Create varaibles.json - run: npm run local-env-file - - - name: Run code formatting - run: npm run prettier - - - name: Run linter - run: npm run lint - - - name: Build project - run: npm run build:mini - - - name: Audit packages - run: npm audit --audit-level=high - - - name: Run tests - run: npm run test:silent - - security-analysis: - name: "Security Analysis" - runs-on: ubuntu-24.04 - needs: validation - permissions: - security-events: write - contents: read - packages: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: javascript-typescript - queries: security-extended - config: | - query-filter: - - exclude: - tags: /cwe-200/ - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - - container-scanning: - name: "Container Security" - runs-on: ubuntu-24.04 - needs: validation - permissions: - security-events: write - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download Grype - run: | - curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b $HOME/bin - echo "$HOME/bin" >> $GITHUB_PATH - - - name: Build Docker image - run: docker build . --file docker/Dockerfile-base --tag localbuild/testimage:latest - - - name: Run vulnerability scan - run: grype -o sarif localbuild/testimage:latest > results.sarif - - - name: Upload SARIF report - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: ./results.sarif - - build-test: - name: "Docker Build Test" - runs-on: ubuntu-24.04 - needs: validation - permissions: - contents: read - packages: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build Docker image - uses: docker/build-push-action@v6 - with: - context: . - file: docker/Dockerfile-base - platforms: linux/amd64,linux/arm64 - push: false - cache-from: type=gha - cache-to: type=gha,mode=max - - todo-management: - name: "TODO Issue Management" - runs-on: ubuntu-24.04 - needs: validation - permissions: - contents: write - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Process TODOs - uses: alstr/todo-to-issue-action@v5 - with: - INSERT_ISSUE_URLS: "true" - - - name: Commit changes - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git add -A - if [[ $(git status --porcelain) ]]; then - git commit -m "Automatically process TODOs [skip ci]" - git push - fi - - deployment: - name: "Docker Deployment" - runs-on: ubuntu-24.04 - needs: [security-analysis, container-scanning, build-test] - permissions: - packages: write - contents: read - strategy: - matrix: - include: - - type: dev - # Only enable when pushing to the dev branch - enabled: ${{ github.ref_name == 'dev' }} - - type: pre-release - # Only enable when a release event is published and it's a prerelease - enabled: ${{ github.event_name == 'release' && github.event.release.prerelease }} - - type: release - # Only enable when a release event is published and it's NOT a prerelease - enabled: ${{ github.event_name == 'release' && !github.event.release.prerelease }} - steps: - - name: Exit early if deployment is not enabled - if: ${{ !matrix.enabled }} - run: | - echo "Skipping deployment for matrix type '${{ matrix.type }}' because conditions are not met." - exit 0 - - - name: Checkout repository - if: ${{ matrix.enabled }} - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - if: ${{ matrix.enabled }} - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - if: ${{ matrix.enabled }} - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Determine tags - if: ${{ matrix.enabled }} - id: tags - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=raw,value=${{ matrix.type == 'dev' && 'nightly' || matrix.type == 'pre-release' && 'pre' || matrix.type == 'release' && 'latest' }} - - - name: Build and push - if: ${{ matrix.enabled }} - uses: docker/build-push-action@v6 - with: - context: . - file: docker/Dockerfile-dev - platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.tags.outputs.tags }} - labels: ${{ steps.tags.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 9e264ac0..4bc7b0ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,154 +1,44 @@ -# custom paths: -src/data/* -src/data/frontendConfiguration.json +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -.tmp -docker/master -docker/slave -.test* -stacks -# Created by https://www.toptal.com/developers/gitignore/api/node -### Node ### -*-audit.json -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt +*.db -# Bower dependency directory (https://bower.io/) -bower_components +# dependencies +/node_modules +/.pnp +.pnp.js -# node-waf configuration -.lock-wscript +# testing +/coverage -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release +# next.js +/.next/ +/out/ -# Dependency directories -node_modules/ -jspm_packages/ +# production +/build -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ +# misc +.DS_Store +*.pem -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* -# dotenv environment variable files -.env +# local env files +.env.local .env.development.local .env.test.local .env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -### Node Patch ### -# Serverless Webpack directories -.webpack/ -# Optional stylelint cache +# vercel +.vercel -# SvelteKit build / generate output -.svelte-kit -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ +**/*.trace +**/*.zip +**/*.tar.gz +**/*.tgz +**/*.log +package-lock.json +**/*.bun diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 4fd02195..00000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true \ No newline at end of file diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 209e3ef4..00000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -20 diff --git a/CREDITS.md b/CREDITS.md deleted file mode 100644 index 6dd2d893..00000000 --- a/CREDITS.md +++ /dev/null @@ -1,106 +0,0 @@ -# CREDITS - -This file shows all npm packages used in DockStatAPI (also Dev packages) - -### License: (MIT AND CC-BY-3.0) - -| Name | Repository | Publisher | -| ----------------- | -------------------------------------------- | -------------------- | -| spdx-ranges@2.1.1 | https://github.com/kemitchell/spdx-ranges.js | The Linux Foundation | - -### License: Apache 2.0 - -| Name | Repository | Publisher | -| ---------------------- | ------------------------------------------ | --------- | -| qrcode-terminal@0.12.0 | https://github.com/gtanner/qrcode-terminal | N/A | - -### License: Apache-2.0 - -| Name | Repository | Publisher | -| ------------------------------------ | ------------------------------------------------------------------------ | -------------------- | -| @ampproject/remapping@2.3.0 | https://github.com/ampproject/remapping | Justin Ridgewell | -| @balena/dockerignore@1.0.2 | https://github.com/balena-io-modules/dockerignore | N/A | -| @eslint/config-array@0.19.2 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.10.0 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/core@0.11.0 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/object-schema@2.1.6 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @eslint/plugin-kit@0.2.5 | https://github.com/eslint/rewrite | Nicholas C. Zakas | -| @grpc/grpc-js@1.12.6 | https://github.com/grpc/grpc-node/tree/master/packages/grpc-js | Google Inc. | -| @grpc/proto-loader@0.7.13 | https://github.com/grpc/grpc-node | Google Inc. | -| @humanfs/core@0.19.1 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanfs/node@0.16.6 | https://github.com/humanwhocodes/humanfs | Nicholas C. Zakas | -| @humanwhocodes/module-importer@1.0.1 | https://github.com/humanwhocodes/module-importer | Nicholas C. Zaks | -| @humanwhocodes/retry@0.3.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @humanwhocodes/retry@0.4.1 | https://github.com/humanwhocodes/retry | Nicholas C. Zaks | -| @puppeteer/browsers@2.7.1 | https://github.com/puppeteer/puppeteer/tree/main/packages/browsers | The Chromium Authors | -| @scarf/scarf@1.4.0 | https://github.com/scarf-sh/scarf-js | Scarf Systems | -| @sigstore/bundle@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | -| @sigstore/core@2.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | -| @sigstore/protobuf-specs@0.3.3 | https://github.com/sigstore/protobuf-specs | bdehamer@github.com | -| @sigstore/sign@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | -| @sigstore/tuf@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | -| @sigstore/verify@2.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | -| b4a@1.6.7 | https://github.com/holepunchto/b4a | Holepunch | -| bare-events@2.5.4 | https://github.com/holepunchto/bare-events | Holepunch | -| bare-fs@4.0.1 | https://github.com/holepunchto/bare-fs | Holepunch | -| bare-os@3.4.0 | https://github.com/holepunchto/bare-os | Holepunch | -| bare-path@3.0.0 | https://github.com/holepunchto/bare-path | Holepunch | -| bare-stream@2.6.5 | https://github.com/holepunchto/bare-stream | Holepunch | -| bser@2.1.1 | https://github.com/facebook/watchman | Wez Furlong | -| chromium-bidi@1.2.0 | https://github.com/GoogleChromeLabs/chromium-bidi | The Chromium Authors | -| detect-libc@2.0.3 | https://github.com/lovell/detect-libc | Lovell Fuller | -| docker-modem@5.0.6 | https://github.com/apocas/docker-modem | Pedro Dias | -| dockerode@4.0.4 | https://github.com/apocas/dockerode | Pedro Dias | -| ejs@3.1.10 | https://github.com/mde/ejs | Matthew Eernisse | -| eslint-visitor-keys@3.4.3 | https://github.com/eslint/eslint-visitor-keys | Toru Nagashima | -| eslint-visitor-keys@4.2.0 | https://github.com/eslint/js | Toru Nagashima | -| exponential-backoff@3.1.1 | https://github.com/coveo/exponential-backoff | Sami Sayegh | -| fb-watchman@2.0.2 | https://github.com/facebook/watchman | Wez Furlong | -| filelist@1.0.4 | https://github.com/mde/filelist | Matthew Eernisse | -| human-signals@2.1.0 | https://github.com/ehmicky/human-signals | ehmicky | -| jake@10.9.2 | https://github.com/jakejs/jake | Matthew Eernisse | -| long@5.2.4 | https://github.com/dcodeIO/long.js | Daniel Wirtz | -| puppeteer-core@24.2.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core | The Chromium Authors | -| puppeteer@24.2.0 | https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer | The Chromium Authors | -| sigstore@3.0.0 | https://github.com/sigstore/sigstore-js | bdehamer@github.com | -| spdx-correct@3.2.0 | https://github.com/jslicense/spdx-correct.js | N/A | -| swagger-ui-dist@5.18.3 | https://github.com/swagger-api/swagger-ui | N/A | -| text-decoder@1.2.3 | https://github.com/holepunchto/text-decoder | Holepunch | -| tunnel-agent@0.6.0 | https://github.com/mikeal/tunnel-agent | Mikeal Rogers | -| typescript@5.7.3 | https://github.com/microsoft/TypeScript | Microsoft Corp. | -| validate-npm-package-license@3.0.4 | https://github.com/kemitchell/validate-npm-package-license.js | Kyle E. Mitchell | -| walker@1.0.8 | https://github.com/daaku/nodejs-walker | Naitik Shah | - -### License: Artistic-2.0 - -| Name | Repository | Publisher | -| ---------- | -------------------------- | ----------- | -| npm@11.1.0 | https://github.com/npm/cli | GitHub Inc. | - -### License: BlueOak-1.0.0 - -| Name | Repository | Publisher | -| ---------------------------- | ------------------------------------------------ | ------------------ | -| chownr@3.0.0 | https://github.com/isaacs/chownr | Isaac Z. Schlueter | -| jackspeak@3.4.3 | https://github.com/isaacs/jackspeak | Isaac Z. Schlueter | -| package-json-from-dist@1.0.1 | https://github.com/isaacs/package-json-from-dist | Isaac Z. Schlueter | -| path-scurry@1.11.1 | https://github.com/isaacs/path-scurry | Isaac Z. Schlueter | -| yallist@5.0.0 | https://github.com/isaacs/yallist | Isaac Z. Schlueter | - -### License: CC-BY-3.0 - -| Name | Repository | Publisher | -| --------------------- | -------------------------------------------------- | -------------------- | -| spdx-exceptions@2.5.0 | https://github.com/kemitchell/spdx-exceptions.json | The Linux Foundation | - -### License: CC-BY-4.0 - -| Name | Repository | Publisher | -| ------------------------- | -------------------------------------------- | ---------- | -| caniuse-lite@1.0.30001698 | https://github.com/browserslist/caniuse-lite | Ben Briggs | - -### License: Python-2.0 - -| Name | Repository | Publisher | -| -------------- | ---------------------------------- | --------- | -| argparse@2.0.1 | https://github.com/nodeca/argparse | N/A | diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 1e9ecebd..00000000 --- a/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2024, ItsNik - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 25778667..6cc99aff 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,3 @@ -# DockStatAPI v2 +# REWRITE -![Dockstat Logo](.github/DockStat.png) - -

- -# Pipelines - -[![Docker Image CI](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/build-image.yml?branch=main&label=Docker%20Image%20CI&style=for-the-badge&logo=docker)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml) -[![Validation](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/validation.yml?branch=dev&label=Validation&style=for-the-badge&logo=checkmarx)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) - -
- -This specific branch contains the currently WIP **DockStatAPI-v2**, this update will bring major breaking changes so please be careful. -With this new release a couple of extra features (compared to v1) are going to be available. - -### Feature List: - -- Swagger API Documentation -- Database (Keeps data for 24 hours max) -- Advanced authentication using hashes and salt -- Custom TypeScript/JavaScript notification modules! (Easy to add and configure!) -- `http` API to configure the backend -- Multi-arch docker builds (using buildx github action) -- Advanced security through middlewares: rate-limiting and authentication -- Multi Arch Docker builds through docker buildx -- High Availability using single master and unlimited worker nodes! -- Dynamically created Graphs - -# 🔗 DockStatAPI v2 Documentation - -_⚠️ = Deprecation warning_ - -- [Introduction](https://outline.itsnik.de/s/dockstat) - - - [DockstatAPI v2](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v2-XRMDKRqMIg) - - - [API reference](https://outline.itsnik.de/s/dockstat/doc/api-reference-1PTxqx1MQ6) - - [How dependency graphs are made](https://outline.itsnik.de/s/dockstat/doc/how-the-dependecy-graphs-are-made-svuZbEHH9g) - - - [DockStat v1](https://outline.itsnik.de/s/dockstat/doc/dockstat-v1-zVaFS4zROI) - - - [⚠️ Customisation](https://outline.itsnik.de/s/dockstat/doc/customization-PiBz4OpQIZ) - - [⚠️ Themes](https://outline.itsnik.de/s/dockstat/doc/themes-BFhN6ZBbYx) - - [⚠️ Installation](https://outline.itsnik.de/s/dockstat/doc/installation-DaO99bB86q) - - - [⚠️ DockStatAPI v1](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v1-jLcVCfPNmS) - - [⚠️ Integrations](https://outline.itsnik.de/s/dockstat/doc/integrations-Agq1oL6HxF) - - [⚠️ Backend API reference](https://outline.itsnik.de/s/dockstat/doc/backend-api-reference-YzcBbDvY33) - -# Dependencies - -Please see [CREDITS.md](./CREDITS.md). - -To create the credits file use: `npm run license` - -Or if you want it as a pre-commit hook create this file: - -```bash -#!/bin/bash -# .git/hooks/pre-commit - -npm run license -``` - -# DockStat(APIs) goals - -DockStack tries to be a lightweigh and more "dashboard" like then [portainer](https://github.com/portainer/portainer), [cAdvisor](https://github.com/google/cadvisor), [dockge](https://github.com/louislam/dockge), ... -I also try to add some "extensions", like in V1 with [🥤cup](https://github.com/sergi0g/cup). -Everything is configured through a backend with Swagger documentation, so that you can follow the code and understand the new v2 frontend better! -DockStat is mainly used for teaching [myself](https://github.com/Its4Nik) more about TypeScript, APIs and backend development! +Using Bun, keep an eye out! diff --git a/TODO.md b/TODO.md deleted file mode 100644 index b850ba72..00000000 --- a/TODO.md +++ /dev/null @@ -1,18 +0,0 @@ -- [x] ~Better Offline mode using "faker" library or self written (probably self written)~ Not needed since there is a docker-compsoe file for local testing integrated inside the repo -- [x] HA compatibility -- [x] !!! Needs testing !!! Add automatic notifications when container state changes, according to selected level for notification service -- [ ] Image update and update notifications -- [ ] trigger container restart / stop / start via backend routes -- [x] Add more logging -- [x] Structure code differently -- [x] Write new README and make the docs better -- [x] Update more files to correct TS syntax => remove "any" -- [x] Websockets -- [x] Better /api/status endpoint with connection status of each host -- [x] Update notification service -- [x] Adjust process.env variables since they don't really work as expected (See [commit](https://github.com/Its4Nik/dockstatapi/pull/21/commits/a03b58c7a17e269f46216df5492e18d008774961)) -- [ ] Better project structure -- [x] Update logging => Better errors -- [x] Update json responses -- [x] Swagger update -- [ ] Edge case testing diff --git a/__tests__/auth.spec.ts b/__tests__/auth.spec.ts deleted file mode 100644 index 84c5f04a..00000000 --- a/__tests__/auth.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -export const testPass = "123456789"; -import { Server } from "http"; -import supertest from "supertest"; -import { startServer } from "../src/utils/startServer"; -import app from "../src/server"; - -const port = 13001; -const server = new Server(app); - -startServer(app, server, port); - -const request = supertest(`http://localhost:${port}`); - -describe("Authentication", () => { - it("Enable Authentication", async () => { - const res = await request.post(`/auth/enable?password=${testPass}`); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - expect(res.body).toHaveProperty( - "message", - "Authentication enabled successfully", - ); - }); - - it("Test no password", async () => { - const res = await request.get("/api/status"); - expect(res.status).toEqual(403); - expect(res.type).toEqual(expect.stringContaining("json")); - }); - - it("Disable authentication", async () => { - const res = await request - .post(`/auth/disable?password=${testPass}`) - .set("x-password", testPass); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - }); -}); diff --git a/__tests__/config.spec.ts b/__tests__/config.spec.ts deleted file mode 100644 index 2650e9ed..00000000 --- a/__tests__/config.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import supertest from "supertest"; -import { startServer } from "../src/utils/startServer"; -import app from "../src/server"; -import { Server } from "http"; - -const port = 13002; -const server = new Server(app); - -startServer(app, server, port); - -const request = supertest(`http://localhost:${port}`); - -const mockServerName: string = "mockstatapi"; -const mockServerIP: string = "127.0.0.1"; -const mockServerPort: number = 2375; - -describe("Config endpoints", () => { - it("Add an host", async () => { - let res = await request.put( - `/conf/addHost?name=${mockServerName}&url=${mockServerIP}&port=${mockServerPort}`, - ); - expect(res.status).toEqual(200); - - res = await request.get("/api/hosts"); - expect(res.status).toEqual(200); - expect(res.body).toContain("mockstatapi"); - }); - - it("Adjust scheduler", async () => { - let res = await request.put("/conf/scheduler?interval=10m"); - expect(res.status).toEqual(200); - - res = await request.get("/api/current-schedule"); - expect(res.status).toEqual(200); - - // Reset to standart 5m - res = await request.put("/conf/scheduler?interval=5m"); - expect(res.status).toEqual(200); - }); - - it("Remove Host from config", async () => { - let res = await request.delete(`/conf/removeHost?hostName=mockstatapi`); - expect(res.status).toEqual(200); - - res = await request.get("/api/hosts"); - expect(res.status).toEqual(200); - expect(res.body).not.toHaveProperty("mockstatapi"); - }); -}); diff --git a/__tests__/database.spec.ts b/__tests__/database.spec.ts deleted file mode 100644 index 55102ce9..00000000 --- a/__tests__/database.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import supertest from "supertest"; -import { startServer } from "../src/utils/startServer"; -import app from "../src/server"; -import { Server } from "http"; - -const port = 13003; -const server = new Server(app); - -startServer(app, server, port); - -const request = supertest(`http://localhost:${port}`); - -describe("Database", () => { - it("Get latest database entry", async () => { - const res = await request.get("/data/latest"); - expect(res.status).toEqual(200); - }); - - it("Get all database entries", async () => { - const res = await request.get("/data/all"); - expect(res.status).toEqual(200); - }); - - it("Clear database", async () => { - let res = await request.delete("/data/clear"); - expect(res.status).toEqual(200); - - res = await request.get("/data/latest"); - expect(res.status).toEqual(404); - expect(res.body).toHaveProperty( - "message", - "No data available for /data/latest", - ); - }); -}); diff --git a/__tests__/frontend.spec.ts b/__tests__/frontend.spec.ts deleted file mode 100644 index af25adc5..00000000 --- a/__tests__/frontend.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import supertest from "supertest"; -import { startServer } from "../src/utils/startServer"; -import app from "../src/server"; -import { Server } from "http"; - -const port = 13004; -const server = new Server(app); - -startServer(app, server, port); - -const request = supertest(`http://localhost:${port}`); - -const sec: number = 1000; - -const mockContainer: string = "dockstatapi"; -const mockLink: string = "https://github.com/its4nik/dockstatapi"; -const mockIcon: string = "dockstatapi.png"; -const mockTag1: string = "backend"; -const mockTag2: string = "local"; - -const verifiedResponse = [ - { - name: "dockstatapi", - tags: ["backend", "local"], - pinned: true, - link: "https://github.com/its4nik/dockstatapi", - icon: "dockstatapi.png", - hidden: true, - }, -]; - -describe("Test frontend specific configurations", () => { - it( - "Setup the configuration file", - async () => { - // Hide container - let res = await request.delete(`/frontend/hide/${mockContainer}`); - - expect(res.status).toEqual(200); - - // Add Tag(s) - res = await request.post(`/frontend/tag/${mockContainer}/${mockTag1}`); - - expect(res.status).toEqual(200); - res = await request.post(`/frontend/tag/${mockContainer}/${mockTag2}`); - - expect(res.status).toEqual(200); - - // Pin container - res = await request.post(`/frontend/pin/${mockContainer}`); - - expect(res.status).toEqual(200); - - // Add link - res = await request.post( - `/frontend/add-link/${mockContainer}/${encodeURIComponent(mockLink)}`, - ); - - expect(res.status).toEqual(200); - - // Add icon - res = await request.post( - `/frontend/add-icon/${mockContainer}/${mockIcon}/false`, - ); - - expect(res.status).toEqual(200); - }, - 60 * sec, - ); - - it("Verify the configuration", async () => { - const res = await request.get("/api/frontend-config"); - - expect(res.status).toEqual(200); - expect(res.body).toEqual(verifiedResponse); - }); - - it( - "Reset configuration", - async () => { - // Show container - let res = await request.post(`/frontend/show/${mockContainer}`); - - expect(res.status).toEqual(200); - - // Remove tag(s) - res = await request.delete( - `/frontend/remove-tag/${mockContainer}/${mockTag1}`, - ); - - expect(res.status).toEqual(200); - - res = await request.delete( - `/frontend/remove-tag/${mockContainer}/${mockTag2}`, - ); - - expect(res.status).toEqual(200); - - // Unpin - res = await request.delete(`/frontend/unpin/${mockContainer}`); - - expect(res.status).toEqual(200); - - // Remove link - res = await request.delete(`/frontend/remove-link/${mockContainer}`); - - expect(res.status).toEqual(200); - - // Remove icon - res = await request.delete(`/frontend/remove-icon/${mockContainer}`); - - expect(res.status).toEqual(200); - }, - 60 * sec, - ); - - it("Verify the reset configuration", async () => { - const res = await request.get("/api/frontend-config"); - - expect(res.status).toEqual(200); - expect(res.body).toEqual([]); - }); -}); diff --git a/__tests__/getters.spec.ts b/__tests__/getters.spec.ts deleted file mode 100644 index f951f42a..00000000 --- a/__tests__/getters.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { createPreviousResponse } from "./util/previousResponse"; -import supertest from "supertest"; -import { startServer } from "../src/utils/startServer"; -import app from "../src/server"; -import { Server } from "http"; - -const port = 13005; -const server = new Server(app); - -startServer(app, server, port); - -const request = supertest(`http://localhost:${port}`); -const PreviousResponse = createPreviousResponse(); - -describe("Get endpoints", () => { - it("GET /api/hosts", async () => { - const res = await request.get("/api/hosts"); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - - const hosts: string[] = res.body; - - if (hosts.length >= 1) { - expect(Array.isArray(hosts)).toBe(true); - expect(hosts.length).toBeGreaterThan(0); - expect(typeof hosts[0]).toBe("string"); - PreviousResponse.set(hosts[0]); - } - }); - - it("GET /api/host/:host/stats", async () => { - const host = PreviousResponse.get(); - - if (!host) { - console.log("No hosts found, skipping /api/host/:host/stats test"); - return; - } - - const res = await request.get(`/api/host/${host}/stats`); - - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - }); - - it("GET /api/system", async () => { - const res = await request.get("/api/system"); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - }); - - it("GET /api/status", async () => { - const res = await request.get("/api/status"); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - expect(res.body).toHaveProperty("ApiReachable", true); - }); - - it("GET /api/containers", async () => { - const res = await request.get("/api/containers"); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - }); - - it("GET /api/config", async () => { - const res = await request.get("/api/config"); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - expect(res.body).toHaveProperty("hosts"); - }); - - it("GET /api/current-schedule", async () => { - const res = await request.get("/api/current-schedule"); - - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - expect(res.body).toHaveProperty("interval"); - }); - - it("GET /api/frontend-config", async () => { - const res = await request.get("/api/frontend-config"); - - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - }); - - it("GET /ha/config", async () => { - const res = await request.get("/ha/config"); - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - }); - - it("GET /notification-service/get-template", async () => { - const res = await request.get("/notification-service/get-template"); - - expect(res.status).toEqual(200); - expect(res.type).toEqual(expect.stringContaining("json")); - expect(res.body).toHaveProperty("text"); - }); -}); diff --git a/__tests__/util/previousResponse.ts b/__tests__/util/previousResponse.ts deleted file mode 100644 index 774a862a..00000000 --- a/__tests__/util/previousResponse.ts +++ /dev/null @@ -1,23 +0,0 @@ -let response: string = ""; - -class PreviousResponse { - set(body: unknown): void { - try { - response = JSON.stringify(body).replace(/[" ]/g, ""); - } catch (error: unknown) { - console.error("Error in setting response:", error); - throw new Error("Failed to set response"); - } - } - - get(): string { - try { - return response; - } catch (error: unknown) { - console.error("Error in getting response:", error); - throw new Error("Failed to get response"); - } - } -} - -export const createPreviousResponse = () => new PreviousResponse(); diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..a5ee6c82 --- /dev/null +++ b/bun.lock @@ -0,0 +1,119 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "dockstatapi", + "dependencies": { + "@elysiajs/swagger": "^1.2.2", + "chalk": "^5.4.1", + "elysia": "latest", + "winston": "^3.17.0", + "winston-transport": "^4.9.0", + }, + "devDependencies": { + "bun-types": "latest", + }, + }, + }, + "packages": { + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + + "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], + + "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], + + "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], + + "@scalar/themes": ["@scalar/themes@0.9.68", "", { "dependencies": { "@scalar/types": "0.0.34" } }, "sha512-466ac2fdQJOBBSLkGUf88vuZVF+qNMeVpjb0aAHrKkxhpjucTPKdTYO8r2dsX1R5k9A13gWPnm594VW5G/bGHw=="], + + "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.27", "", {}, "sha512-C7mxE1VC3WC2McOufZXEU48IfRVI+BcKxk4NOyNn3+JMUNdJHEWGS5CqjuDX+ij2NCCz8/nse1mT7yn8Fv2GHg=="], + + "@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + + "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + + "@unhead/schema": ["@unhead/schema@1.11.19", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "bun-types": ["bun-types@1.2.3", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P7AeyTseLKAvgaZqQrvp3RqFM3yN9PlcLuSTe7SoJOfZkER73mLdT2vEQi8U64S1YvM/ldcNiQjn0Sn7H9lGgg=="], + + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], + + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "elysia": ["elysia@1.2.21", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-E9b1JcB7fiQ2ptk24W8OnBrMYUoKzffIXob9uTVUKhqOKxaXAd9UyWBeyr7JCDa/VD/b/9S8aIey9/YJsK5sLg=="], + + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + + "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], + + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], + + "@scalar/themes/@scalar/types": ["@scalar/types@0.0.34", "", { "dependencies": { "@scalar/openapi-types": "0.1.8", "@unhead/schema": "^1.11.11" } }, "sha512-q01ctijmHArM5KOny2zU+sHfhpsgOAENrDENecK2TsQNn5FYLmFZouMKeW2M6F7KFLPZnFxUiL/rT88b6Rp/Kg=="], + + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.8", "", {}, "sha512-iufA5/6hPCmRIVD2eh7qGpoKvoA08Gw/qUb2JECifBtAwA93fo7+1k9uHK440f2LMJsbxIzA+nv7RS0BmfiO/g=="], + } +} diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base deleted file mode 100644 index f21146ba..00000000 --- a/docker/Dockerfile-base +++ /dev/null @@ -1,76 +0,0 @@ -# Stage 1: Build stage -FROM node:20-alpine AS builder - -LABEL maintainer="https://github.com/its4nik" -LABEL version="2.0.1" -LABEL description="API for DockStat" -LABEL license="BSD-3-Clause license" -LABEL repository="https://github.com/its4nik/dockstatapi" -LABEL documentation="https://github.com/its4nik/dockstatapi" -LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" -LABEL org.opencontainers.image.licenses="BSD-3-Clause license" -LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" - -WORKDIR /app - -ENV NODE_NO_WARNINGS=1 - -RUN apk add --no-cache curl bash - -COPY package*.json tsconfig.json environment.d.ts ./ - -RUN npm ci --include=dev - -COPY ./src ./src -RUN mv ./src/sample-variable.json ./src/data/variables.json - -RUN npm run build:mini - -# -------------------------------------- -# Stage 2: Dependency pruning stage -FROM node:20-alpine AS deps -WORKDIR /api -COPY --from=builder /app/package*.json . -RUN npm ci --omit=dev - -# -------------------------------------- -# Stage 3: Final production image -FROM node:20-alpine AS prod - -WORKDIR /api - -RUN apk add --no-cache docker-cli bash curl && \ - mkdir -p /usr/libexec/docker/cli-plugins && \ - curl -sSL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" \ - -o /usr/libexec/docker/cli-plugins/docker-compose && \ - chmod +x /usr/libexec/docker/cli-plugins/docker-compose && \ - rm -rf /var/cache/apk/* - -ARG USER_ID=10001 -ARG GROUP_ID=10001 -RUN addgroup -g $GROUP_ID dockstatapi && \ - adduser -u $USER_ID -G dockstatapi -h /api -s /bin/sh -D dockstatapi - -COPY --from=builder --chown=dockstatapi:dockstatapi /app/dist/src ./src -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/config/swagger.yaml ./src/config/swagger.yaml -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/utils/assets ./src/utils/assets -COPY --from=builder /app/package.json ./ -COPY --from=deps --chown=dockstatapi:dockstatapi /api/node_modules ./node_modules - -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/entrypoint.sh . -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/createEnvFile.sh . -RUN chmod +x *.sh - -RUN mkdir -p /api/src/data && \ - chown -R dockstatapi:dockstatapi /api && \ - chmod -R 755 /api && \ - chmod 775 /api/src/data - -HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:9876/api/status || exit 1 - -EXPOSE 9876 -STOPSIGNAL 130 -USER dockstatapi - -ENTRYPOINT [ "sh", "./entrypoint.sh", "--prod" ] diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev deleted file mode 100644 index 00b88008..00000000 --- a/docker/Dockerfile-dev +++ /dev/null @@ -1,76 +0,0 @@ -# Stage 1: Build stage -FROM node:20-alpine AS builder - -LABEL maintainer="https://github.com/its4nik" -LABEL version="2.0.1" -LABEL description="API for DockStat" -LABEL license="BSD-3-Clause license" -LABEL repository="https://github.com/its4nik/dockstatapi" -LABEL documentation="https://github.com/its4nik/dockstatapi" -LABEL org.opencontainers.image.description="The DockSatAPI is a free and OpenSource backend for gathering container statistics across hosts" -LABEL org.opencontainers.image.licenses="BSD-3-Clause license" -LABEL org.opencontainers.image.source="https://github.com/its4nik/dockstatapi" - -WORKDIR /app - -ENV NODE_NO_WARNINGS=1 - -RUN apk add --no-cache curl bash - -COPY package*.json tsconfig.json environment.d.ts ./ - -RUN npm ci --include=dev - -COPY ./src ./src -RUN mv ./src/sample-variable.json ./src/data/variables.json - -RUN npm run build - -# -------------------------------------- -# Stage 2: Dependency pruning stage -FROM node:20-alpine AS deps -WORKDIR /api -COPY --from=builder /app/package*.json . -RUN npm ci --omit=dev - -# -------------------------------------- -# Stage 3: Final production image -FROM node:20-alpine AS prod - -WORKDIR /api - -RUN apk add --no-cache docker-cli bash curl && \ - mkdir -p /usr/libexec/docker/cli-plugins && \ - curl -sSL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" \ - -o /usr/libexec/docker/cli-plugins/docker-compose && \ - chmod +x /usr/libexec/docker/cli-plugins/docker-compose && \ - rm -rf /var/cache/apk/* - -ARG USER_ID=10001 -ARG GROUP_ID=10001 -RUN addgroup -g $GROUP_ID dockstatapi && \ - adduser -u $USER_ID -G dockstatapi -h /api -s /bin/sh -D dockstatapi - -COPY --from=builder --chown=dockstatapi:dockstatapi /app/dist/src ./src -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/config/swagger.yaml ./src/config/swagger.yaml -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/utils/assets ./src/utils/assets -COPY --from=builder /app/package.json ./ -COPY --from=deps --chown=dockstatapi:dockstatapi /api/node_modules ./node_modules - -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/entrypoint.sh . -COPY --from=builder --chown=dockstatapi:dockstatapi /app/src/misc/createEnvFile.sh . -RUN chmod +x *.sh - -RUN mkdir -p /api/src/data && \ - chown -R dockstatapi:dockstatapi /api && \ - chmod -R 755 /api && \ - chmod 775 /api/src/data - -HEALTHCHECK --interval=5m --timeout=3s \ - CMD curl -f http://localhost:9876/api/status || exit 1 - -EXPOSE 9876 -STOPSIGNAL 130 -USER dockstatapi - -ENTRYPOINT [ "sh", "./entrypoint.sh", "--dev" ] diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml deleted file mode 100644 index 7bc3773f..00000000 --- a/docker/docker-compose.dev.yaml +++ /dev/null @@ -1,40 +0,0 @@ -services: - test-socket-proxy: - image: lscr.io/linuxserver/socket-proxy:latest - container_name: test-socket-proxy - environment: - - ALLOW_START=1 #optional - - ALLOW_STOP=1 #optional - - ALLOW_RESTARTS=1 #optional - - AUTH=0 #optional - - BUILD=0 #optional - - COMMIT=0 #optional - - CONFIGS=0 #optional - - CONTAINERS=1 #optional - - DISABLE_IPV6=0 #optional - - DISTRIBUTION=0 #optional - - EVENTS=1 #optional - - EXEC=0 #optional - - IMAGES=0 #optional - - INFO=1 #optional - - NETWORKS=1 #optional - - NODES=1 #optional - - PING=1 #optional - - POST=0 #optional - - PLUGINS=0 #optional - - SECRETS=0 #optional - - SERVICES=0 #optional - - SESSION=0 #optional - - SWARM=0 #optional - - SYSTEM=0 #optional - - TASKS=0 #optional - - VERSION=1 #optional - - VOLUMES=0 #optional - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - restart: unless-stopped - read_only: true - tmpfs: - - /run - ports: - - 2375:2375 \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml deleted file mode 100644 index 436d8a21..00000000 --- a/docker/docker-compose.yaml +++ /dev/null @@ -1,82 +0,0 @@ -networks: - shared-network: - driver: bridge - -services: - master: - container_name: master - user: "${UID:-1000}:${GID:-1000}" - environment: - - NODE_ENV=development - - HA_MASTER=true - - HA_MASTER_IP=master:9876 - - HA_NODE=slave:9876 - - HA_UNSAFE=true - volumes: - - ./master/data:/api/src/data - - ./master/logs:/api/logs - ports: - - 9876:9876 - image: dockstatapi:local - networks: - - shared-network - depends_on: - - slave - - test-socket-proxy - - slave: - container_name: slave - user: "${UID:-1000}:${GID:-1000}" - environment: - - NODE_ENV=development - volumes: - - ./slave/data:/api/src/data - - ./slave/logs:/api/logs - ports: - - 6789:9876 - image: dockstatapi:local - depends_on: - - test-socket-proxy - networks: - - shared-network - - test-socket-proxy: - image: lscr.io/linuxserver/socket-proxy:latest - container_name: test-socket-proxy - environment: - - ALLOW_START=1 #optional - - ALLOW_STOP=1 #optional - - ALLOW_RESTARTS=1 #optional - - AUTH=0 #optional - - BUILD=0 #optional - - COMMIT=0 #optional - - CONFIGS=0 #optional - - CONTAINERS=1 #optional - - DISABLE_IPV6=0 #optional - - DISTRIBUTION=0 #optional - - EVENTS=1 #optional - - EXEC=0 #optional - - IMAGES=0 #optional - - INFO=1 #optional - - NETWORKS=1 #optional - - NODES=1 #optional - - PING=1 #optional - - POST=0 #optional - - PLUGINS=0 #optional - - SECRETS=0 #optional - - SERVICES=0 #optional - - SESSION=0 #optional - - SWARM=0 #optional - - SYSTEM=0 #optional - - TASKS=0 #optional - - VERSION=1 #optional - - VOLUMES=0 #optional - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - restart: unless-stopped - read_only: true - tmpfs: - - /run - networks: - - shared-network - diff --git a/environment.d.ts b/environment.d.ts deleted file mode 100644 index df2595f5..00000000 --- a/environment.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare global { - namespace NodeJS { - interface ProcessEnv { - // Node specific: - NODE_ENV: "development" | "production" | "testing"; - PORT: string | undefined; - CI: "true" | null; - } - } -} - -export {}; diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 56994a62..00000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; - -/** @type {import('eslint').Linter.Config[]} */ -export default [ - { ignores: ["node_modules/*", "dist/*"] }, - { files: ["src/**/*.ts"] }, - { languageOptions: { globals: globals.node } }, - pluginJs.configs.recommended, - ...tseslint.configs.recommended, -]; diff --git a/nodemon.json b/nodemon.json deleted file mode 100644 index be32c75d..00000000 --- a/nodemon.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "ignore": [ - "**/data/**", - "src/logs", - "**/fixtures/**", - ".gitignore", - "**/*.json", - "**/__tests__/**" - ], - "execMap": { - "ts": "tsx" - }, - "delay": 2500 -} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 6efc7ed3..00000000 --- a/package-lock.json +++ /dev/null @@ -1,13317 +0,0 @@ -{ - "name": "dockstatapi", - "version": "2.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "dockstatapi", - "version": "2.0.1", - "license": "BSD 3-Clause License", - "dependencies": { - "bcrypt": "^5.1.1", - "chokidar": "^4.0.1", - "cors": "^2.8.5", - "cytoscape": "^3.30.4", - "docker-compose": "^1.1.0", - "dockerode": "^4.0.2", - "express": "^4.21.1", - "express-rate-limit": "^7.4.1", - "https": "^1.0.0", - "i": "^0.3.7", - "ipaddr.js": "^2.2.0", - "nodemailer": "^6.9.16", - "npm": "^11.0.0", - "puppeteer": "^24.0.0", - "sqlite3": "^5.1.7", - "swagger-ui-express": "^5.0.1", - "winston": "^3.15.0", - "winston-daily-rotate-file": "^5.0.0", - "yamljs": "^0.3.0" - }, - "devDependencies": { - "@eslint/js": "^9.17.0", - "@types/bcrypt": "^5.0.2", - "@types/cors": "^2.8.17", - "@types/cytoscape": "^3.21.8", - "@types/dockerode": "^3.3.31", - "@types/express": "^5.0.0", - "@types/express-handlebars": "^5.3.1", - "@types/jest": "^29.5.14", - "@types/node": "^22.9.0", - "@types/node-fetch": "^2.6.12", - "@types/nodemailer": "^6.4.17", - "@types/supertest": "^6.0.2", - "@types/supports-color": "^8.1.3", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.7", - "@types/ws": "^8.5.14", - "@types/yamljs": "^0.2.34", - "@typescript-eslint/eslint-plugin": "^8.18.2", - "@typescript-eslint/parser": "^8.18.2", - "dependency-cruiser": "^16.5.0", - "eslint": "^9.17.0", - "globals": "^15.14.0", - "jest": "^29.7.0", - "license-checker": "^25.0.1", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "supertest": "^7.0.0", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "tsx": "^4.19.2", - "typescript-eslint": "^8.18.2", - "uglify-js": "^3.19.3" - }, - "engines": { - "npm": ">=10.8.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/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", - "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz", - "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.5", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.7", - "@babel/parser": "^7.26.7", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.26.7", - "@babel/types": "^7.26.7", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", - "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.5", - "@babel/types": "^7.26.5", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", - "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", - "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.26.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", - "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.5", - "@babel/parser": "^7.26.7", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.7", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", - "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@balena/dockerignore": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", - "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", - "license": "Apache-2.0" - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", - "license": "MIT", - "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.10.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "license": "MIT", - "optional": true - }, - "node_modules/@grpc/grpc-js": { - "version": "1.12.6", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.6.tgz", - "integrity": "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.7.13", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", - "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "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/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "license": "MIT", - "optional": true, - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/move-file/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "optional": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" - }, - "node_modules/@puppeteer/browsers": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.7.1.tgz", - "integrity": "sha512-MK7rtm8JjaxPN7Mf1JdZIZKPD2Z+W7osvrC1vjpvfOX1K0awDIHYbNi89f7eotp7eMUn2shWnt03HwVbriXtKQ==", - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.4.0", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.0", - "tar-fs": "^3.0.8", - "yargs": "^17.7.2" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", - "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "license": "MIT" - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/bcrypt": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", - "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cookiejar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", - "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cytoscape": { - "version": "3.21.9", - "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.9.tgz", - "integrity": "sha512-JyrG4tllI6jvuISPjHK9j2Xv/LTbnLekLke5otGStjFluIyA9JjgnvgZrSBsp8cEDpiTjwgZUZwpPv8TSBcoLw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/docker-modem": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", - "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/ssh2": "*" - } - }, - "node_modules/@types/dockerode": { - "version": "3.3.34", - "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.34.tgz", - "integrity": "sha512-mH9SuIb8NuTDsMus5epcbTzSbEo52fKLBMo0zapzYIAIyfDqoIFn7L3trekHLKC8qmxGV++pPUP4YqQ9n5v2Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/docker-modem": "*", - "@types/node": "*", - "@types/ssh2": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", - "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-handlebars": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@types/express-handlebars/-/express-handlebars-5.3.1.tgz", - "integrity": "sha512-DSzaERLO4gHb8AqnrL58jzSDyT0yDdl6HqDc+bGz1Hf0nrG1FK30nHGzv8NBEGR8QV9eUGB/YaE0Qj3NjF7siw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", - "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/methods": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", - "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.13.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz", - "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@types/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/nodemailer": { - "version": "6.4.17", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", - "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/qs": { - "version": "6.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/ssh2": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.4.tgz", - "integrity": "sha512-9JTQgVBWSgq6mAen6PVnrAmty1lqgCMvpfN+1Ck5WRUsyMYPa6qd50/vMJ0y1zkGpOEgLzm8m8Dx/Y5vRouLaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^18.11.18" - } - }, - "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.75", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.75.tgz", - "integrity": "sha512-UIksWtThob6ZVSyxcOqCLOUNg/dyO1Qvx4McgeuhrEtHTLFTf7BBhEazaE4K806FGTPtzd/2sE90qn4fVr7cyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/ssh2/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/superagent": { - "version": "8.1.9", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", - "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/cookiejar": "^2.1.5", - "@types/methods": "^1.1.4", - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/supertest": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", - "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/methods": "^1.1.4", - "@types/superagent": "^8.1.0" - } - }, - "node_modules/@types/supports-color": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/@types/supports-color/-/supports-color-8.1.3.tgz", - "integrity": "sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/swagger-jsdoc": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", - "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/swagger-ui-express": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz", - "integrity": "sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", - "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yamljs": { - "version": "0.2.34", - "resolved": "https://registry.npmjs.org/@types/yamljs/-/yamljs-0.2.34.tgz", - "integrity": "sha512-gJvfRlv9ErxdOv7ux7UsJVePtX54NAvQyd8ncoiFqK8G5aeHIfQfGH2fbruvjAQ9657HwAaO54waS+Dsk2QTUQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz", - "integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/type-utils": "8.23.0", - "@typescript-eslint/utils": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", - "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", - "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz", - "integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/utils": "8.23.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", - "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz", - "integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.23.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-jsx-walk": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", - "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn-loose": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz", - "integrity": "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "license": "MIT", - "optional": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "license": "ISC" - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", - "license": "Apache-2.0" - }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "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" - }, - "node_modules/bare-events": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", - "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", - "license": "Apache-2.0", - "optional": true - }, - "node_modules/bare-fs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.1.tgz", - "integrity": "sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-events": "^2.0.0", - "bare-path": "^3.0.0", - "bare-stream": "^2.0.0" - }, - "engines": { - "bare": ">=1.7.0" - } - }, - "node_modules/bare-os": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.4.0.tgz", - "integrity": "sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "bare": ">=1.6.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", - "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "streamx": "^2.21.0" - }, - "peerDependencies": { - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/bcrypt": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", - "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "node-addon-api": "^5.0.0" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/buildcheck": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", - "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", - "optional": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "optional": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC", - "optional": true - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001698", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001698.tgz", - "integrity": "sha512-xJ3km2oiG/MbNU8G6zIq6XRZ6HtAOVXsbOrP/blGazi52kc5Yy7b6sDA5O+FbROzRrV7BSTllLHuNvmawYUJjw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/chromium-bidi": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-1.2.0.tgz", - "integrity": "sha512-XtdJ1GSN6S3l7tO7F77GhNsw0K367p0IsLYf2yZawCVAKKC3lUvDhPdMVrB2FNhmhfW43QGYbEX3Wg6q0maGwQ==", - "license": "Apache-2.0", - "dependencies": { - "mitt": "^3.0.1", - "zod": "^3.24.1" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "license": "MIT", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC" - }, - "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==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cpu-features": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", - "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "buildcheck": "~0.0.6", - "nan": "^2.19.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "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/cytoscape": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.0.tgz", - "integrity": "sha512-zDGn1K/tfZwEnoGOcHc0H4XazqAAXAuDpcYw9mUnUjATjqljyCNGJv8uEvbvxGaGHaVshxMecyl6oc6uKzRfbw==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/debuglog": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", - "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT" - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dependency-cruiser": { - "version": "16.9.0", - "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-16.9.0.tgz", - "integrity": "sha512-Gc/xHNOBq1nk5i7FPCuexCD0m2OXB/WEfiSHfNYQaQaHZiZltnl5Ixp/ZG38Jvi8aEhKBQTHV4Aw6gmR7rWlOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "acorn-jsx-walk": "^2.0.0", - "acorn-loose": "^8.4.0", - "acorn-walk": "^8.3.4", - "ajv": "^8.17.1", - "commander": "^13.0.0", - "enhanced-resolve": "^5.18.0", - "ignore": "^7.0.0", - "interpret": "^3.1.1", - "is-installed-globally": "^1.0.0", - "json5": "^2.2.3", - "memoize": "^10.0.0", - "picocolors": "^1.1.1", - "picomatch": "^4.0.2", - "prompts": "^2.4.2", - "rechoir": "^0.8.0", - "safe-regex": "^2.1.1", - "semver": "^7.6.3", - "teamcity-service-messages": "^0.1.14", - "tsconfig-paths-webpack-plugin": "^4.2.0", - "watskeburt": "^4.2.2" - }, - "bin": { - "depcruise": "bin/dependency-cruise.mjs", - "depcruise-baseline": "bin/depcruise-baseline.mjs", - "depcruise-fmt": "bin/depcruise-fmt.mjs", - "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs", - "dependency-cruise": "bin/dependency-cruise.mjs", - "dependency-cruiser": "bin/dependency-cruise.mjs" - }, - "engines": { - "node": "^18.17||>=20" - } - }, - "node_modules/dependency-cruiser/node_modules/ignore": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", - "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/devtools-protocol": { - "version": "0.0.1402036", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1402036.tgz", - "integrity": "sha512-JwAYQgEvm3yD45CHB+RmF5kMbWtXBaOGwuxa87sZogHcLCv8c/IqnThaoQ1y60d7pXWjSKWQphPEc+1rAScVdg==", - "license": "BSD-3-Clause" - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/docker-compose": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.1.0.tgz", - "integrity": "sha512-VrkQJNafPQ5d6bGULW0P6KqcxSkv3ZU5Wn2wQA19oB71o7+55vQ9ogFe2MMeNbK+jc9rrKVy280DnHO5JLMWOQ==", - "license": "MIT", - "dependencies": { - "yaml": "^2.2.2" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/docker-modem": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz", - "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==", - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.1", - "readable-stream": "^3.5.0", - "split-ca": "^1.0.1", - "ssh2": "^1.15.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/dockerode": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.4.tgz", - "integrity": "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w==", - "license": "Apache-2.0", - "dependencies": { - "@balena/dockerignore": "^1.0.2", - "@grpc/grpc-js": "^1.11.1", - "@grpc/proto-loader": "^0.7.13", - "docker-modem": "^5.0.6", - "protobufjs": "^7.3.2", - "tar-fs": "~2.0.1", - "uuid": "^10.0.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.96", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.96.tgz", - "integrity": "sha512-8AJUW6dh75Fm/ny8+kZKJzI1pgoE8bKLZlzDU2W1ENd+DXKJrx7I7l9hb8UWR4ojlnb5OlixMt00QWiYJoVw1w==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "license": "MIT", - "optional": true - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/eslint": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.0.tgz", - "integrity": "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extract-zip/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", - "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/file-stream-rotator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", - "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", - "license": "MIT", - "dependencies": { - "moment": "^2.29.1" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true, - "license": "ISC" - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, - "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formidable": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", - "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "dezalgo": "^1.0.4", - "hexoid": "^2.0.0", - "once": "^1.4.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "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/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "function-bind": "^1.1.2", - "get-proto": "^1.0.0", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/get-uri": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", - "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-directory": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "4.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "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/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC" - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hexoid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", - "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true, - "license": "ISC" - }, - "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-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "license": "BSD-2-Clause", - "optional": true - }, - "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==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "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/http-proxy-agent/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/https": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", - "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", - "license": "ISC" - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/i": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz", - "integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "license": "ISC", - "optional": true - }, - "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", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-installed-globally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", - "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "global-directory": "^4.0.1", - "is-path-inside": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "license": "MIT", - "optional": true - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", - "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "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==", - "devOptional": 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-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "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-report/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/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jake/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/jake/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/license-checker": { - "version": "25.0.1", - "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", - "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "chalk": "^2.4.1", - "debug": "^3.1.0", - "mkdirp": "^0.5.1", - "nopt": "^4.0.1", - "read-installed": "~4.0.3", - "semver": "^5.5.0", - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0", - "spdx-satisfies": "^4.0.0", - "treeify": "^1.1.0" - }, - "bin": { - "license-checker": "bin/license-checker" - } - }, - "node_modules/license-checker/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/license-checker/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/license-checker/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/license-checker/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/license-checker/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/license-checker/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/license-checker/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/license-checker/node_modules/nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "1", - "osenv": "^0.1.4" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/license-checker/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/license-checker/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "license": "MIT", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/long": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", - "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==", - "license": "Apache-2.0" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "license": "ISC", - "optional": true, - "dependencies": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/make-fetch-happen/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC", - "optional": true - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memoize": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.0.0.tgz", - "integrity": "sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/memoize?sponsor=1" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "optionalDependencies": { - "encoding": "^0.1.12" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "license": "MIT" - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nan": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", - "license": "MIT", - "optional": true - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/node-abi": { - "version": "3.74.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", - "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", - "license": "MIT", - "optional": true, - "dependencies": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": ">= 10.12.0" - } - }, - "node_modules/node-gyp/node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/nodemailer": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", - "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", - "license": "MIT-0", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/nodemon": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", - "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/nodemon/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/nodemon/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/nodemon/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/nodemon/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.1.0.tgz", - "integrity": "sha512-rPMBrZud26lI/LcjQeLw/K5Hf1apXMKgkpNNEzp0YQYmM877+T1ZNKPcB2hnTi7e6fBNz8xLtMMn/w46fVUqGw==", - "bundleDependencies": [ - "@isaacs/string-locale-compare", - "@npmcli/arborist", - "@npmcli/config", - "@npmcli/fs", - "@npmcli/map-workspaces", - "@npmcli/package-json", - "@npmcli/promise-spawn", - "@npmcli/redact", - "@npmcli/run-script", - "@sigstore/tuf", - "abbrev", - "archy", - "cacache", - "chalk", - "ci-info", - "cli-columns", - "fastest-levenshtein", - "fs-minipass", - "glob", - "graceful-fs", - "hosted-git-info", - "ini", - "init-package-json", - "is-cidr", - "json-parse-even-better-errors", - "libnpmaccess", - "libnpmdiff", - "libnpmexec", - "libnpmfund", - "libnpmorg", - "libnpmpack", - "libnpmpublish", - "libnpmsearch", - "libnpmteam", - "libnpmversion", - "make-fetch-happen", - "minimatch", - "minipass", - "minipass-pipeline", - "ms", - "node-gyp", - "nopt", - "normalize-package-data", - "npm-audit-report", - "npm-install-checks", - "npm-package-arg", - "npm-pick-manifest", - "npm-profile", - "npm-registry-fetch", - "npm-user-validate", - "p-map", - "pacote", - "parse-conflict-json", - "proc-log", - "qrcode-terminal", - "read", - "semver", - "spdx-expression-parse", - "ssri", - "supports-color", - "tar", - "text-table", - "tiny-relative-date", - "treeverse", - "validate-npm-package-name", - "which" - ], - "license": "Artistic-2.0", - "workspaces": [ - "docs", - "smoke-tests", - "mock-globals", - "mock-registry", - "workspaces/*" - ], - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.0.0", - "@npmcli/config": "^10.0.1", - "@npmcli/fs": "^4.0.0", - "@npmcli/map-workspaces": "^4.0.2", - "@npmcli/package-json": "^6.1.1", - "@npmcli/promise-spawn": "^8.0.2", - "@npmcli/redact": "^3.0.0", - "@npmcli/run-script": "^9.0.1", - "@sigstore/tuf": "^3.0.0", - "abbrev": "^3.0.0", - "archy": "~1.0.0", - "cacache": "^19.0.1", - "chalk": "^5.4.1", - "ci-info": "^4.1.0", - "cli-columns": "^4.0.0", - "fastest-levenshtein": "^1.0.16", - "fs-minipass": "^3.0.3", - "glob": "^10.4.5", - "graceful-fs": "^4.2.11", - "hosted-git-info": "^8.0.2", - "ini": "^5.0.0", - "init-package-json": "^8.0.0", - "is-cidr": "^5.1.0", - "json-parse-even-better-errors": "^4.0.0", - "libnpmaccess": "^10.0.0", - "libnpmdiff": "^8.0.0", - "libnpmexec": "^10.0.0", - "libnpmfund": "^7.0.0", - "libnpmorg": "^8.0.0", - "libnpmpack": "^9.0.0", - "libnpmpublish": "^11.0.0", - "libnpmsearch": "^9.0.0", - "libnpmteam": "^8.0.0", - "libnpmversion": "^8.0.0", - "make-fetch-happen": "^14.0.3", - "minimatch": "^9.0.5", - "minipass": "^7.1.1", - "minipass-pipeline": "^1.2.4", - "ms": "^2.1.2", - "node-gyp": "^11.0.0", - "nopt": "^8.0.0", - "normalize-package-data": "^7.0.0", - "npm-audit-report": "^6.0.0", - "npm-install-checks": "^7.1.1", - "npm-package-arg": "^12.0.1", - "npm-pick-manifest": "^10.0.0", - "npm-profile": "^11.0.1", - "npm-registry-fetch": "^18.0.2", - "npm-user-validate": "^3.0.0", - "p-map": "^7.0.3", - "pacote": "^21.0.0", - "parse-conflict-json": "^4.0.0", - "proc-log": "^5.0.0", - "qrcode-terminal": "^0.12.0", - "read": "^4.0.0", - "semver": "^7.6.3", - "spdx-expression-parse": "^4.0.0", - "ssri": "^12.0.0", - "supports-color": "^9.4.0", - "tar": "^6.2.1", - "text-table": "~0.2.0", - "tiny-relative-date": "^1.3.0", - "treeverse": "^3.0.0", - "validate-npm-package-name": "^6.0.0", - "which": "^5.0.0" - }, - "bin": { - "npm": "bin/npm-cli.js", - "npx": "bin/npx-cli.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "dev": true, - "license": "ISC" - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui": { - "version": "8.0.2", - "inBundle": 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/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "inBundle": 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/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/npm/node_modules/@isaacs/string-locale-compare": { - "version": "1.1.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/@npmcli/agent": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/fs": "^4.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/metavuln-calculator": "^9.0.0", - "@npmcli/name-from-folder": "^3.0.0", - "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.1", - "@npmcli/query": "^4.0.0", - "@npmcli/redact": "^3.0.0", - "@npmcli/run-script": "^9.0.1", - "bin-links": "^5.0.0", - "cacache": "^19.0.1", - "common-ancestor-path": "^1.0.1", - "hosted-git-info": "^8.0.0", - "json-stringify-nice": "^1.1.4", - "lru-cache": "^10.2.2", - "minimatch": "^9.0.4", - "nopt": "^8.0.0", - "npm-install-checks": "^7.1.0", - "npm-package-arg": "^12.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.1", - "pacote": "^21.0.0", - "parse-conflict-json": "^4.0.0", - "proc-log": "^5.0.0", - "proggy": "^3.0.0", - "promise-all-reject-late": "^1.0.0", - "promise-call-limit": "^3.0.1", - "read-package-json-fast": "^4.0.0", - "semver": "^7.3.7", - "ssri": "^12.0.0", - "treeverse": "^3.0.0", - "walk-up-path": "^4.0.0" - }, - "bin": { - "arborist": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/package-json": "^6.0.1", - "ci-info": "^4.0.0", - "ini": "^5.0.0", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "walk-up-path": "^4.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/fs": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/git": { - "version": "6.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/promise-spawn": "^8.0.0", - "ini": "^5.0.0", - "lru-cache": "^10.0.1", - "npm-pick-manifest": "^10.0.0", - "proc-log": "^5.0.0", - "promise-inflight": "^1.0.1", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/installed-package-contents": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-bundled": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" - }, - "bin": { - "installed-package-contents": "bin/index.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "4.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/name-from-folder": "^3.0.0", - "@npmcli/package-json": "^6.0.0", - "glob": "^10.2.2", - "minimatch": "^9.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { - "version": "9.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cacache": "^19.0.0", - "json-parse-even-better-errors": "^4.0.0", - "pacote": "^21.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/name-from-folder": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/node-gyp": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "6.1.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^6.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^8.0.0", - "json-parse-even-better-errors": "^4.0.0", - "proc-log": "^5.0.0", - "semver": "^7.5.3", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/promise-spawn": { - "version": "8.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "which": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/query": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^6.1.2" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/redact": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "9.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "node-gyp": "^11.0.0", - "proc-log": "^5.0.0", - "which": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/npm/node_modules/@sigstore/bundle": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/core": { - "version": "2.0.0", - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/protobuf-specs": { - "version": "0.3.3", - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/sign": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^3.0.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "make-fetch-happen": "^14.0.1", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2", - "tuf-js": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/verify": { - "version": "2.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^3.0.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.3.2" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@tufjs/models": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/abbrev": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/agent-base": { - "version": "7.1.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/ansi-regex": { - "version": "5.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/ansi-styles": { - "version": "6.2.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/npm/node_modules/aproba": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/archy": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/balanced-match": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/bin-links": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cmd-shim": "^7.0.0", - "npm-normalize-package-bin": "^4.0.0", - "proc-log": "^5.0.0", - "read-cmd-shim": "^5.0.0", - "write-file-atomic": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/binary-extensions": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/brace-expansion": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/npm/node_modules/cacache": { - "version": "19.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/chownr": { - "version": "3.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/minizlib": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/mkdirp": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/tar": { - "version": "7.4.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/yallist": { - "version": "5.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/chalk": { - "version": "5.4.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/npm/node_modules/chownr": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/ci-info": { - "version": "4.1.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/cidr-regex": { - "version": "4.1.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "ip-regex": "^5.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/npm/node_modules/cli-columns": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm/node_modules/cmd-shim": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/color-convert": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/npm/node_modules/color-name": { - "version": "1.1.4", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/common-ancestor-path": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/cross-spawn": { - "version": "7.0.6", - "inBundle": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/cssesc": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/debug": { - "version": "4.4.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/npm/node_modules/diff": { - "version": "7.0.0", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/npm/node_modules/eastasianwidth": { - "version": "0.2.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/emoji-regex": { - "version": "8.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/encoding": { - "version": "0.1.13", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/npm/node_modules/env-paths": { - "version": "2.2.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/npm/node_modules/err-code": { - "version": "2.0.3", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/exponential-backoff": { - "version": "3.1.1", - "inBundle": true, - "license": "Apache-2.0" - }, - "node_modules/npm/node_modules/fastest-levenshtein": { - "version": "1.0.16", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/npm/node_modules/foreground-child": { - "version": "3.3.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/fs-minipass": { - "version": "3.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/glob": { - "version": "10.4.5", - "inBundle": 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/npm/node_modules/graceful-fs": { - "version": "4.2.11", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/hosted-git-info": { - "version": "8.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/http-cache-semantics": { - "version": "4.1.1", - "inBundle": true, - "license": "BSD-2-Clause" - }, - "node_modules/npm/node_modules/http-proxy-agent": { - "version": "7.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/https-proxy-agent": { - "version": "7.0.6", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/iconv-lite": { - "version": "0.6.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm/node_modules/ignore-walk": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minimatch": "^9.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/imurmurhash": { - "version": "0.1.4", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/npm/node_modules/ini": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/init-package-json": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/package-json": "^6.1.0", - "npm-package-arg": "^12.0.0", - "promzard": "^2.0.0", - "read": "^4.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/ip-address": { - "version": "9.0.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/npm/node_modules/ip-regex": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/is-cidr": { - "version": "5.1.0", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "cidr-regex": "^4.1.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/npm/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/isexe": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/jackspeak": { - "version": "3.4.3", - "inBundle": 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/npm/node_modules/jsbn": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/json-parse-even-better-errors": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/json-stringify-nice": { - "version": "1.1.4", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/jsonparse": { - "version": "1.3.1", - "engines": [ - "node >= 0.2.0" - ], - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff": { - "version": "6.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff-apply": { - "version": "5.5.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/libnpmaccess": { - "version": "10.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-package-arg": "^12.0.0", - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "binary-extensions": "^3.0.0", - "diff": "^7.0.0", - "minimatch": "^9.0.4", - "npm-package-arg": "^12.0.0", - "pacote": "^21.0.0", - "tar": "^6.2.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmexec": { - "version": "10.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.0.0", - "@npmcli/run-script": "^9.0.1", - "ci-info": "^4.0.0", - "npm-package-arg": "^12.0.0", - "pacote": "^21.0.0", - "proc-log": "^5.0.0", - "read": "^4.0.0", - "read-package-json-fast": "^4.0.0", - "semver": "^7.3.7", - "walk-up-path": "^4.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmorg": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmpack": { - "version": "9.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.0.0", - "@npmcli/run-script": "^9.0.1", - "npm-package-arg": "^12.0.0", - "pacote": "^21.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmpublish": { - "version": "11.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "ci-info": "^4.0.0", - "normalize-package-data": "^7.0.0", - "npm-package-arg": "^12.0.0", - "npm-registry-fetch": "^18.0.1", - "proc-log": "^5.0.0", - "semver": "^7.3.7", - "sigstore": "^3.0.0", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmsearch": { - "version": "9.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmteam": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmversion": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^6.0.1", - "@npmcli/run-script": "^9.0.1", - "json-parse-even-better-errors": "^4.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/lru-cache": { - "version": "10.4.3", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/make-fetch-happen": { - "version": "14.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/make-fetch-happen/node_modules/negotiator": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/npm/node_modules/minimatch": { - "version": "9.0.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/minipass": { - "version": "7.1.2", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/npm/node_modules/minipass-collect": { - "version": "2.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/npm/node_modules/minipass-fetch": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/minipass-fetch/node_modules/minizlib": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/minipass-flush": { - "version": "1.0.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-pipeline": { - "version": "1.2.4", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized": { - "version": "1.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minizlib": { - "version": "2.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/mkdirp": { - "version": "1.0.4", - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/ms": { - "version": "2.1.3", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/mute-stream": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/node-gyp": { - "version": "11.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^10.3.10", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "tar": "^7.4.3", - "which": "^5.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/chownr": { - "version": "3.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/minizlib": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/mkdirp": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/tar": { - "version": "7.4.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/yallist": { - "version": "5.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/nopt": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/nopt/node_modules/abbrev": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/normalize-package-data": { - "version": "7.0.0", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^8.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-audit-report": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-bundled": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-normalize-package-bin": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-install-checks": { - "version": "7.1.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-normalize-package-bin": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-package-arg": { - "version": "12.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-packlist": { - "version": "10.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "ignore-walk": "^7.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-pick-manifest": { - "version": "10.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-install-checks": "^7.1.0", - "npm-normalize-package-bin": "^4.0.0", - "npm-package-arg": "^12.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-profile": { - "version": "11.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^18.0.0", - "proc-log": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-registry-fetch": { - "version": "18.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/redact": "^3.0.0", - "jsonparse": "^1.3.1", - "make-fetch-happen": "^14.0.0", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minizlib": "^3.0.1", - "npm-package-arg": "^12.0.0", - "proc-log": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-registry-fetch/node_modules/minizlib": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/npm-user-validate": { - "version": "3.0.0", - "inBundle": true, - "license": "BSD-2-Clause", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/p-map": { - "version": "7.0.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/package-json-from-dist": { - "version": "1.0.1", - "inBundle": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/npm/node_modules/pacote": { - "version": "21.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^6.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "@npmcli/run-script": "^9.0.0", - "cacache": "^19.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^12.0.0", - "npm-packlist": "^10.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "sigstore": "^3.0.0", - "ssri": "^12.0.0", - "tar": "^6.1.11" - }, - "bin": { - "pacote": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/parse-conflict-json": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^4.0.0", - "just-diff": "^6.0.0", - "just-diff-apply": "^5.2.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/path-key": { - "version": "3.1.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/path-scurry": { - "version": "1.11.1", - "inBundle": 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/npm/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/proc-log": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/proggy": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/promise-all-reject-late": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-call-limit": { - "version": "3.0.2", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-inflight": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/promise-retry": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/promzard": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "read": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/qrcode-terminal": { - "version": "0.12.0", - "inBundle": true, - "bin": { - "qrcode-terminal": "bin/qrcode-terminal.js" - } - }, - "node_modules/npm/node_modules/read": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "mute-stream": "^2.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/read-cmd-shim": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/read-package-json-fast": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/retry": { - "version": "0.12.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/npm/node_modules/rimraf": { - "version": "5.0.10", - "inBundle": true, - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/safer-buffer": { - "version": "2.1.2", - "inBundle": true, - "license": "MIT", - "optional": true - }, - "node_modules/npm/node_modules/semver": { - "version": "7.6.3", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/shebang-command": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/shebang-regex": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/signal-exit": { - "version": "4.1.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/sigstore": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^3.0.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "@sigstore/sign": "^3.0.0", - "@sigstore/tuf": "^3.0.0", - "@sigstore/verify": "^2.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/smart-buffer": { - "version": "4.2.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks": { - "version": "2.8.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks-proxy-agent": { - "version": "8.0.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/spdx-correct": { - "version": "3.2.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-exceptions": { - "version": "2.5.0", - "inBundle": true, - "license": "CC-BY-3.0" - }, - "node_modules/npm/node_modules/spdx-expression-parse": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.21", - "inBundle": true, - "license": "CC0-1.0" - }, - "node_modules/npm/node_modules/sprintf-js": { - "version": "1.1.3", - "inBundle": true, - "license": "BSD-3-Clause" - }, - "node_modules/npm/node_modules/ssri": { - "version": "12.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/string-width": { - "version": "4.2.3", - "inBundle": 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/npm/node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "inBundle": 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/npm/node_modules/strip-ansi": { - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/supports-color": { - "version": "9.4.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/npm/node_modules/tar": { - "version": "6.2.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/text-table": { - "version": "0.2.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/tiny-relative-date": { - "version": "1.3.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/treeverse": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tufjs/models": "3.0.1", - "debug": "^4.3.6", - "make-fetch-happen": "^14.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/unique-filename": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/unique-slug": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/util-deprecate": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/validate-npm-package-license": { - "version": "3.0.4", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-name": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/walk-up-path": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/npm/node_modules/which": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/which/node_modules/isexe": { - "version": "3.1.1", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/npm/node_modules/wrap-ansi": { - "version": "8.1.0", - "inBundle": 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/npm/node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "inBundle": 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/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "9.2.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "inBundle": 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/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/write-file-atomic": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/yallist": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "license": "MIT", - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.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==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pac-proxy-agent": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", - "integrity": "sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==", - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/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/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.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-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "license": "ISC", - "optional": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "license": "MIT", - "optional": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/protobufjs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", - "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/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/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/puppeteer": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.2.0.tgz", - "integrity": "sha512-z8vv7zPEgrilIbOo3WNvM+2mXMnyM9f4z6zdrB88Fzeuo43Oupmjrzk3EpuvuCtyK0A7Lsllfx7Z+4BvEEGJcQ==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.7.1", - "chromium-bidi": "1.2.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1402036", - "puppeteer-core": "24.2.0", - "typed-query-selector": "^2.12.0" - }, - "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/puppeteer-core": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.2.0.tgz", - "integrity": "sha512-e4A4/xqWdd4kcE6QVHYhJ+Qlx/+XpgjP4d8OwBx0DJoY/nkIRhSgYmKQnv7+XSs1ofBstalt+XPGrkaz4FoXOQ==", - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.7.1", - "chromium-bidi": "1.2.0", - "debug": "^4.4.0", - "devtools-protocol": "0.0.1402036", - "typed-query-selector": "^2.12.0", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/read-installed": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", - "integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "debuglog": "^1.0.1", - "read-package-json": "^2.0.0", - "readdir-scoped-modules": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "slide": "~1.1.3", - "util-extend": "^1.0.1" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.2" - } - }, - "node_modules/read-installed/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/read-package-json": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", - "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", - "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.1", - "json-parse-even-better-errors": "^2.3.0", - "normalize-package-data": "^2.0.0", - "npm-normalize-package-bin": "^1.0.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdir-scoped-modules": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", - "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "dev": true, - "license": "ISC", - "dependencies": { - "debuglog": "^1.0.1", - "dezalgo": "^1.0.0", - "graceful-fs": "^4.1.2", - "once": "^1.3.0" - } - }, - "node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/regexp-tree": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", - "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", - "dev": true, - "license": "MIT", - "bin": { - "regexp-tree": "bin/regexp-tree" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", - "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "regexp-tree": "~0.1.1" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "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/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", - "dev": true, - "license": "ISC", - "engines": { - "node": "*" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", - "license": "MIT", - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "devOptional": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spdx-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", - "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-find-index": "^1.0.2", - "spdx-expression-parse": "^3.0.0", - "spdx-ranges": "^2.0.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/spdx-ranges": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", - "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", - "dev": true, - "license": "(MIT AND CC-BY-3.0)" - }, - "node_modules/spdx-satisfies": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz", - "integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-compare": "^1.0.0", - "spdx-expression-parse": "^3.0.0", - "spdx-ranges": "^2.0.0" - } - }, - "node_modules/split-ca": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", - "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", - "license": "ISC" - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause" - }, - "node_modules/sqlite3": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", - "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1", - "tar": "^6.1.11" - }, - "optionalDependencies": { - "node-gyp": "8.x" - }, - "peerDependencies": { - "node-gyp": "8.x" - }, - "peerDependenciesMeta": { - "node-gyp": { - "optional": true - } - } - }, - "node_modules/sqlite3/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" - }, - "node_modules/ssh2": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", - "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", - "hasInstallScript": true, - "dependencies": { - "asn1": "^0.2.6", - "bcrypt-pbkdf": "^1.0.2" - }, - "engines": { - "node": ">=10.16.0" - }, - "optionalDependencies": { - "cpu-features": "~0.0.10", - "nan": "^2.20.0" - } - }, - "node_modules/ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/streamx": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", - "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", - "license": "MIT", - "dependencies": { - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "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==", - "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/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/superagent": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", - "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^3.5.1", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/superagent/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/supertest": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", - "integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "methods": "^1.1.2", - "superagent": "^9.0.1" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "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/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swagger-ui-dist": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.3.tgz", - "integrity": "sha512-G33HFW0iFNStfY2x6QXO2JYVMrFruc8AZRX0U/L71aA7WeWfX2E5Nm8E/tsipSZJeIZZbSjUDeynLK/wcuNWIw==", - "license": "Apache-2.0", - "dependencies": { - "@scarf/scarf": "=1.4.0" - } - }, - "node_modules/swagger-ui-express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", - "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", - "license": "MIT", - "dependencies": { - "swagger-ui-dist": ">=5.0.0" - }, - "engines": { - "node": ">= v0.10.32" - }, - "peerDependencies": { - "express": ">=4.0.0 || >=5.0.0-beta" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar-fs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", - "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.0.0" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/teamcity-service-messages": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz", - "integrity": "sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/treeify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", - "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-jest": { - "version": "29.2.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", - "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bs-logger": "^0.2.6", - "ejs": "^3.1.10", - "fast-json-stable-stringify": "^2.1.0", - "jest-util": "^29.0.0", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.6.3", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", - "typescript": ">=4.3 <6" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tsconfig-paths-webpack-plugin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", - "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.7.0", - "tapable": "^2.2.1", - "tsconfig-paths": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tsconfig-paths/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "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.19.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", - "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.23.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "license": "Unlicense" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-query-selector": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", - "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", - "license": "MIT" - }, - "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "devOptional": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.23.0.tgz", - "integrity": "sha512-/LBRo3HrXr5LxmrdYSOCvoAMm7p2jNizNfbIpCgvG4HMsnoprRUOce/+8VJ9BDYWW68rqIENE/haVLWPeFZBVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.23.0", - "@typescript-eslint/parser": "8.23.0", - "@typescript-eslint/utils": "8.23.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "license": "MIT" - }, - "node_modules/unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "unique-slug": "^2.0.0" - } - }, - "node_modules/unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "license": "ISC", - "optional": true, - "dependencies": { - "imurmurhash": "^0.1.4" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/util-extend": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", - "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", - "dev": true, - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/watskeburt": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-4.2.2.tgz", - "integrity": "sha512-AOCg1UYxWpiHW1tUwqpJau8vzarZYTtzl2uu99UptBmbzx6kOzCGMfRLF6KIRX4PYekmryn89MzxlRNkL66YyA==", - "dev": true, - "license": "MIT", - "bin": { - "watskeburt": "dist/run-cli.js" - }, - "engines": { - "node": "^18||>=20" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/winston": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", - "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-daily-rotate-file": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", - "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", - "license": "MIT", - "dependencies": { - "file-stream-rotator": "^0.6.1", - "object-hash": "^3.0.0", - "triple-beam": "^1.4.1", - "winston-transport": "^4.7.0" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "winston": "^3" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "license": "MIT", - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "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/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/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yamljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", - "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "glob": "^7.0.5" - }, - "bin": { - "json2yaml": "bin/json2yaml", - "yaml2json": "bin/yaml2json" - } - }, - "node_modules/yamljs/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/yamljs/node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/package.json b/package.json index c48ee738..0e1deb8b 100644 --- a/package.json +++ b/package.json @@ -1,123 +1,19 @@ { "name": "dockstatapi", - "repository": "git@github.com:Its4Nik/dockstatapi.git", - "version": "2.0.1", - "description": "API for docker hosts using dockerode", - "main": "src/server.ts", + "version": "2.1.0", "scripts": { - "test": "NODE_ENV=testing jest -w 1 --forceExit", - "test:silent": "NODE_ENV=testing jest -w 1 --forceExit --silent", - "local-env-file": "bash ./src/misc/createEnvDev.sh", - "start": "npm run local-env-file && NODE_ENV=production tsx src/server.ts", - "start:build": "npm run local-env-file -d && npm run build && NODE_ENV=production node dist/src/src/server.js", - "dev": "npm run local-env-file && NODE_ENV=development nodemon", - "dev:socket": "docker compose -f docker/docker-compose.dev.yaml up -d && npm run local-env-file && NODE_ENV=development nodemon ; docker compose -f docker/docker-compose.dev.yaml down", - "dev:trace": "npm run local-env-file && NODE_ENV=development nodemon --trace-uncaught --trace-warnings", - "dep": "bash ./src/misc/dependencyGraphs/createDependencyGraph.sh", - "dep:remove": "bash ./src/misc/removeUnusedDeps.sh && npm run dep", - "build": "tsc", - "build:mini": "tsc && bash ./src/misc/minifyDist.sh --build-only", - "build:docker": "docker build . -t \"dockstatapi:local\" -f ./docker/Dockerfile-dev", - "build:docker:prod": "docker build . -t \"dockstatapi:local\" -f ./docker/Dockerfile-base", - "mini": "bash ./src/misc/minifyDist.sh", - "docker": "docker compose -f docker/docker-compose.yaml up -d && bash ./src/misc/.tmux.sh; docker compose -f docker/docker-compose.yaml down", - "docker:build": "npm run build:docker && npm run docker", - "docker:build:prod": "npm run build:docker:prod && npm run docker", - "prettier": "prettier -c ./__tests__/*.spec.ts --parser typescript --write && prettier -c ./src/**/*.ts --parser typescript --write && prettier -c ./.github/workflows/*.yaml --parser yaml --write && prettier -c ./**/*.md --parser markdown --write && prettier -c ./**/*.json --parser json --write", - "lint": "eslint", - "lint:fix": "eslint --fix", - "license": "bash ./src/misc/credits.sh", - "finish": "npm run local-env-file && npm run license && npm run prettier && npm run lint" + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "bun run --watch src/index.ts" }, - "keywords": [], - "author": "Its4Nik", - "license": "BSD 3-Clause License", "dependencies": { - "bcrypt": "^5.1.1", - "chokidar": "^4.0.1", - "cors": "^2.8.5", - "cytoscape": "^3.30.4", - "docker-compose": "^1.1.0", - "dockerode": "^4.0.2", - "express": "^4.21.1", - "express-rate-limit": "^7.4.1", - "https": "^1.0.0", - "i": "^0.3.7", - "ipaddr.js": "^2.2.0", - "nodemailer": "^6.9.16", - "npm": "^11.0.0", - "puppeteer": "^24.0.0", - "sqlite3": "^5.1.7", - "swagger-ui-express": "^5.0.1", - "winston": "^3.15.0", - "winston-daily-rotate-file": "^5.0.0", - "yamljs": "^0.3.0" + "@elysiajs/swagger": "^1.2.2", + "chalk": "^5.4.1", + "elysia": "latest", + "winston": "^3.17.0", + "winston-transport": "^4.9.0" }, "devDependencies": { - "@eslint/js": "^9.17.0", - "@types/bcrypt": "^5.0.2", - "@types/cors": "^2.8.17", - "@types/cytoscape": "^3.21.8", - "@types/dockerode": "^3.3.31", - "@types/express": "^5.0.0", - "@types/express-handlebars": "^5.3.1", - "@types/jest": "^29.5.14", - "@types/node": "^22.9.0", - "@types/node-fetch": "^2.6.12", - "@types/nodemailer": "^6.4.17", - "@types/supertest": "^6.0.2", - "@types/supports-color": "^8.1.3", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.7", - "@types/ws": "^8.5.14", - "@types/yamljs": "^0.2.34", - "@typescript-eslint/eslint-plugin": "^8.18.2", - "@typescript-eslint/parser": "^8.18.2", - "dependency-cruiser": "^16.5.0", - "eslint": "^9.17.0", - "globals": "^15.14.0", - "jest": "^29.7.0", - "license-checker": "^25.0.1", - "nodemon": "^3.1.7", - "prettier": "^3.4.2", - "supertest": "^7.0.0", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "tsx": "^4.19.2", - "typescript-eslint": "^8.18.2", - "uglify-js": "^3.19.3" + "bun-types": "latest" }, - "engines": { - "npm": ">=10.8.2" - }, - "jest": { - "preset": "ts-jest", - "testMatch": [ - "**/__tests__/**/*.(test|spec).ts" - ], - "testEnvironment": "node", - "transform": { - "^.+\\.(ts|tsx)$": "ts-jest" - }, - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "jsx", - "json", - "node" - ], - "coveragePathIgnorePatterns": [ - "/node_modules/" - ], - "moduleNameMapper": { - "^@/(.*)$": "src/$1" - }, - "transformIgnorePatterns": [ - "/node_modules/" - ], - "testPathIgnorePatterns": [ - "util" - ] - } + "module": "src/index.js" } diff --git a/src/config/db.ts b/src/config/db.ts deleted file mode 100644 index 5ed4d6a0..00000000 --- a/src/config/db.ts +++ /dev/null @@ -1,23 +0,0 @@ -import sqlite3 from "sqlite3"; -import logger from "../utils/logger"; - -const dbPath: string = "./src/data/database.db"; - -const db: sqlite3.Database = new sqlite3.Database(dbPath, (error: unknown) => { - if (error as Error) { - logger.error("Error opening database:", (error as Error).message); - } else { - db.run( - `CREATE TABLE IF NOT EXISTS data ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - info TEXT NOT NULL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP - )`, - () => { - logger.info("Database created / checked successfully, table is ready."); - }, - ); - } -}); - -export default db; diff --git a/src/config/hostsystem.ts b/src/config/hostsystem.ts deleted file mode 100644 index 87928a8e..00000000 --- a/src/config/hostsystem.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - RUNNING_IN_DOCKER, - VERSION, - HA_MASTER, - HA_UNSAFE, - TRUSTED_PROXIES, - LOG_LEVEL, -} from "./variables"; -import fs from "fs"; -import logger from "../utils/logger"; -import os from "os"; -import { atomicWrite } from "../utils/atomicWrite"; - -const userConf = "./src/data/user.conf"; -const inDocker: boolean = RUNNING_IN_DOCKER == "true"; -const version: string = VERSION || "unknown"; -const masterNode: string = HA_MASTER === "true" ? "✓" : "✗"; -const unsafeSync: string = HA_UNSAFE === "true" ? "✓" : "✗"; - -let trustedProxies: string = ""; - -if (TRUSTED_PROXIES) { - trustedProxies = TRUSTED_PROXIES; -} else { - trustedProxies = "✗"; -} - -function writeUserConf(port: number) { - let previousConfig = null; - let shouldRewriteConfig = false; - - const installationDetails = { - installedAt: new Date().toISOString(), - backendVersion: version, - inDocker: inDocker, - installedBy: os.userInfo().username, - platform: os.platform(), - arch: os.arch(), - }; - - if (fs.existsSync(userConf)) { - try { - previousConfig = JSON.parse(fs.readFileSync(userConf, "utf-8")); - if (previousConfig.backendVersion !== version) { - shouldRewriteConfig = true; - logger.debug( - "Version change detected. Rewriting configuration file...", - ); - } else { - logger.debug("No version change detected. Skipping re-initialization."); - } - } catch (error) { - logger.error( - "Error reading the configuration file. Rewriting it...", - error, - ); - shouldRewriteConfig = true; - } - } else { - logger.debug("Configuration file not found. Creating a new one..."); - shouldRewriteConfig = true; - } - - if (shouldRewriteConfig) { - atomicWrite(userConf, JSON.stringify(installationDetails, null, 2)); - logger.debug("Configuration file created/updated:", userConf); - } - - const startDetails = { - startedAt: new Date().toISOString(), - backendVersion: version, - }; - - logger.info("-----------------------------------------"); - logger.info(`Starting at : ${startDetails.startedAt}`); - logger.info(`Running env : ${process.env.NODE_ENV}`); - logger.info(`Version : ${startDetails.backendVersion}`); - logger.info(`Docker : ${installationDetails.inDocker}`); - logger.info(`Running as : ${installationDetails.installedBy}`); - logger.info(`Platform : ${installationDetails.platform}`); - logger.info(`Arch : ${installationDetails.arch}`); - logger.info(`Master node : ${masterNode}`); - logger.info(`Unsafe sync : ${unsafeSync}`); - logger.info(`Proxies : ${trustedProxies}`); - logger.info(`Log Level : ${LOG_LEVEL}`); - logger.info(`Server : http://localhost:${port}`); - if (process.env.NODE_ENV !== "production") { - logger.info(`Swagger-UI : http://localhost:${port}/api-docs`); - } - logger.info("-----------------------------------------"); -} - -export default writeUserConf; diff --git a/src/config/initFiles.ts b/src/config/initFiles.ts deleted file mode 100644 index 7524907c..00000000 --- a/src/config/initFiles.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { existsSync } from "fs"; -import logger from "../utils/logger"; -import { atomicWrite } from "../utils/atomicWrite"; - -const files = [ - { path: "./src/data/highAvailability.json", content: "{}" }, - { - path: "./src/data/password.json", - content: JSON.stringify( - { - hash: "", - salt: "", - }, - null, - 2, - ), - }, - { path: "./src/data/states.json", content: "{}" }, - { - path: "./src/data/template.json", - content: JSON.stringify( - { text: "{{name}} is {{state}} on {{hostName}}" }, - null, - 2, - ), - }, - { path: "./src/data/frontendConfiguration.json", content: "[]" }, - { path: "./src/data/usePassword.txt", content: "false" }, -]; - -function initFiles(): void { - files.forEach(({ path: filePath, content }) => { - if (!existsSync(filePath)) { - atomicWrite(filePath, content); - logger.info(`Created: ${filePath}`); - } else { - logger.debug(`Skipped (already exists): ${filePath}`); - } - }); -} - -export default initFiles; diff --git a/src/config/stacks.ts b/src/config/stacks.ts deleted file mode 100644 index def75dcb..00000000 --- a/src/config/stacks.ts +++ /dev/null @@ -1,260 +0,0 @@ -import logger from "../utils/logger"; -import fs from "fs"; -import path from "path"; -import YAML from "yamljs"; -import { DockerComposeFile } from "../typings/dockerCompose"; -import { dockerStackProperty, dockerStackEnv } from "../typings/dockerStackEnv"; -import { stackConfig } from "../typings/stackConfig"; -import { validate } from "../handlers/stack"; -import { atomicWrite } from "../utils/atomicWrite"; -import { AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT } from "./variables"; - -const nameRegex = /^[A-Za-z0-9_-]+$/; -const stackRootFolder = "./stacks"; -const configFilePath = `${stackRootFolder}/.config.json`; - -async function getStackCompose(name: string) { - try { - await validate(name); - const stackCompose = `${stackRootFolder}/${name}/docker-compose.yaml`; - - return YAML.parse(fs.readFileSync(stackCompose, "utf-8")); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } -} - -async function getStackConfig(): Promise { - try { - return fs.readFileSync(configFilePath, "utf-8"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } -} - -async function createStack( - name: string, - content: DockerComposeFile, - override: boolean, -) { - try { - if (!name) { - const errorMsg = "Name required"; - logger.error(errorMsg); - throw new Error(errorMsg); - } - - if (!nameRegex.test(name)) { - const errorMsg = "Name does not match [A-Za-z0-9_-]"; - logger.error(errorMsg); - throw new Error(errorMsg); - } - - if (!content) { - const errorMsg = "Data for this stack is required"; - logger.error(errorMsg); - throw new Error(errorMsg); - } - - const stackFolderPath = `${stackRootFolder}/${name}`; - - if (!fs.existsSync(stackFolderPath)) { - fs.mkdirSync(stackFolderPath, { recursive: true }); - logger.debug(`Created stack folder at ${stackFolderPath}`); - } - - updateConfigFile(name); - - let yamlContent = ""; - let environmentFileData: dockerStackEnv = { environment: [] }; - if (AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT == "true" && override == false) { - logger.debug("AEFM is activated"); - const { cleanCompose, envSchema } = extractAndRemoveEnv(content); - yamlContent = YAML.stringify(cleanCompose, 10, 2); - environmentFileData = envSchema; - - await writeEnvFile(name, environmentFileData); - } else { - yamlContent = YAML.stringify(content, 10, 2); - } - - const filePath = `${stackFolderPath}/docker-compose.yaml`; - atomicWrite(filePath, yamlContent); - logger.debug(`Stack content written to ${filePath}`); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } -} - -function updateConfigFile(stackName: string) { - try { - let config: stackConfig = { stacks: [] }; - if (fs.existsSync(configFilePath)) { - const configData = fs.readFileSync(configFilePath, "utf-8"); - config = JSON.parse(configData); - } - - const stacks = config.stacks || []; - - if (!stacks.includes(stackName)) { - stacks.push(stackName); - } - - const updatedConfig = { stacks }; - atomicWrite(configFilePath, JSON.stringify(updatedConfig, null, 2)); - logger.debug(`Updated .config.json with stack name: ${stackName}`); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(`Error updating .config.json: ${errorMsg}`); - throw new Error(errorMsg); - } -} - -async function writeEnvFile( - name: string, - data: dockerStackEnv, -): Promise { - try { - await validate(name); - - if (!nameRegex.test(name)) { - const sanitizedStackName = name.replace(/\n|\r/g, ""); - const errorMsg = `Invalid stack name: ${sanitizedStackName}`; - logger.error(errorMsg); - return false; - } - - const dockerEnvPath = path.resolve(stackRootFolder, name, "docker.env"); - const dockerEnvPathBak = path.resolve( - stackRootFolder, - name, - ".docker.env.bak", - ); - - if ( - !dockerEnvPath.startsWith(path.resolve(stackRootFolder)) || - !dockerEnvPathBak.startsWith(path.resolve(stackRootFolder)) - ) { - const sanitizedStackName = name.replace(/\n|\r/g, ""); - const errorMsg = `Path traversal attempt detected: ${sanitizedStackName}`; - logger.error(errorMsg); - return false; - } - - const variableNames = data.environment.map(({ name }) => name); - const duplicateVars = variableNames.filter( - (item, index) => variableNames.indexOf(item) !== index, - ); - - if (duplicateVars.length > 0) { - const duplicatesList = duplicateVars.join(", "); - const sanitizedDuplicatesList = duplicatesList.replace(/\n|\r/g, ""); - const errorMsg = `Duplicate environment variables detected: ${sanitizedDuplicatesList}`; - logger.error(errorMsg); - return false; - } - - const envFileContent = data.environment - .map(({ name, value }) => `${name}="${value}"`) - .join("\n"); - - if (fs.existsSync(dockerEnvPath)) { - logger.debug("Creating a local backup"); - const previousData = fs.readFileSync(dockerEnvPath); - atomicWrite(dockerEnvPathBak, previousData); - } - - atomicWrite(dockerEnvPath, envFileContent); - return true; - } catch (error: unknown) { - const errorMsg = ( - error instanceof Error ? error.message : String(error) - ).replace(/\n|\r/g, ""); - logger.error(errorMsg); - throw new Error(errorMsg); - } -} - -async function getEnvFile(name: string) { - await validate(name); - const dockerEnvPath = path.resolve(stackRootFolder, name, "docker.env"); - if (!dockerEnvPath.startsWith(path.resolve(stackRootFolder))) { - throw new Error("Invalid path"); - } - - if (fs.existsSync(dockerEnvPath)) { - const data = fs.readFileSync(dockerEnvPath, "utf-8"); - - const environment: dockerStackProperty[] = data - .split("\n") - .filter((line) => line.trim() !== "" && line.includes("=")) - .map((line) => { - const [name, ...valueParts] = line.split("="); - const value = valueParts.join("=").replace(/^"|"$/g, ""); - return { name: name.trim(), value: value.trim() }; - }); - - return { environment }; - } else { - return null; - } -} - -function extractAndRemoveEnv(data: DockerComposeFile): { - cleanCompose: DockerComposeFile; - envSchema: dockerStackEnv; -} { - const environment: dockerStackProperty[] = []; - const envCount: Record = {}; - - for (const [, service] of Object.entries(data.services)) { - if (service.environment) { - for (const key of Object.keys(service.environment)) { - envCount[key] = (envCount[key] || 0) + 1; - } - } - } - - for (const [, service] of Object.entries(data.services)) { - if (service.environment) { - const remainingEnvironment: Record = {}; - - for (const [key, value] of Object.entries(service.environment)) { - if (envCount[key] === 1) { - environment.push({ name: key, value }); - } else { - remainingEnvironment[key] = value; - } - } - - service.environment = remainingEnvironment; - - if (Object.keys(service.environment).length === 0) { - delete service.environment; - } - } - - if (!service.env_file) { - service.env_file = ["./docker.env"]; - } - } - - return { - cleanCompose: data, - envSchema: { environment }, - }; -} - -export { - createStack, - getStackConfig, - getStackCompose, - writeEnvFile, - getEnvFile, -}; diff --git a/src/config/swagger.yaml b/src/config/swagger.yaml deleted file mode 100644 index 2230f73b..00000000 --- a/src/config/swagger.yaml +++ /dev/null @@ -1,2084 +0,0 @@ -openapi: "3.0.0" - -security: - - passwordAuth: [] - -info: - title: "DockStatAPI" - version: "2.0.1" - externalDocs: - description: DockStat(API) Wiki - url: https://outline.itsnik.de/s/dockstat - license: - name: BSD-3-Clause - url: https://github.com/Its4Nik/dockstatapi/tree/main?tab=BSD-3-Clause-1-ov-file#readme - contact: - email: info@itsnik.de - description: |- - ![DockStat](https://github.com/Its4Nik/dockstatapi/blob/dev/.github/DockStat-dark.png?raw=true) - - # Pipelines - - [![Docker Image CI](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/build-image.yml?branch=main&label=Docker%20Image%20CI&style=for-the-badge&logo=docker)](https://github.com/Its4Nik/dockstatapi/actions/workflows/build-image.yml) - [![Validation](https://img.shields.io/github/actions/workflow/status/Its4Nik/dockstatapi/validation.yml?branch=dev&label=Validation&style=for-the-badge&logo=checkmarx)](https://github.com/Its4Nik/dockstatapi/actions/workflows/validation.yml) - - # Feature List: - - - Swagger API Documentation - - Database (Keeps data for 24 hours max) - - Advanced authentication using hashes and salt - - `http` API to configure the backend - - Multi-arch docker builds (using buildx github action) - - Advanced security through middlewares: rate-limiting and authentication - - Multi Arch Docker builds through docker buildx - - High Availability using single master and unlimited worker nodes! - - # 🔗 DockStatAPI v2 Documentation - - _⚠️ = Deprecation warning_ - - - [Introduction](https://outline.itsnik.de/s/dockstat) - - - [DockstatAPI v2](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v2-XRMDKRqMIg) - - - [API reference](https://outline.itsnik.de/s/dockstat/doc/api-reference-1PTxqx1MQ6) - - [How dependency graphs are made](https://outline.itsnik.de/s/dockstat/doc/how-the-dependecy-graphs-are-made-svuZbEHH9g) - - - [DockStat v1](https://outline.itsnik.de/s/dockstat/doc/dockstat-v1-zVaFS4zROI) - - - [⚠️ Customisation](https://outline.itsnik.de/s/dockstat/doc/customization-PiBz4OpQIZ) - - [⚠️ Themes](https://outline.itsnik.de/s/dockstat/doc/themes-BFhN6ZBbYx) - - [⚠️ Installation](https://outline.itsnik.de/s/dockstat/doc/installation-DaO99bB86q) - - - [⚠️ DockStatAPI v1](https://outline.itsnik.de/s/dockstat/doc/dockstatapi-v1-jLcVCfPNmS) - - [⚠️ Integrations](https://outline.itsnik.de/s/dockstat/doc/integrations-Agq1oL6HxF) - - [⚠️ Backend API reference](https://outline.itsnik.de/s/dockstat/doc/backend-api-reference-YzcBbDvY33) - -tags: - - name: Authentication - description: Routes to setup / configure authentication - - - name: Configuration - description: Configuring the backend - - - name: Database queries - description: Queries made against the SQLite database - - - name: "Frontend Configuration" - description: Backend routes to configure the integrated "frontend service" - - - name: Miscellaneous - description: Some "random" routes which still can be useful - - - name: High availability - description: High availability routes, mainly used by HA sync - - - name: Notification Service - description: Routes to configure the notification service - - - name: Stacks - description: Management of the Stack module - -servers: - - url: http://localhost:9876 - description: "Your DockStatAPI instance" - -paths: - # ------------------------------ - # Authentication setup: - /auth/enable: - post: - tags: - - "Authentication" - summary: Enable authentication for every route - operationId: enableAuth - parameters: - - name: password - in: query - required: true - explode: true - schema: - type: string - default: super-secret - responses: - "200": - description: Success - Successfully enabled authentication - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Authentication enabled successfully" - - "403": - description: Error - Password is required / Authentication is already enabled - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /auth/disable: - post: - tags: - - "Authentication" - summary: Disable authentication for every route - operationId: disableAuth - parameters: - - name: password - in: query - required: true - explode: true - schema: - type: string - default: super-secret - responses: - "200": - description: Succes - Succesfully disabled authentication - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Authentication disabled successfully" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - # ------------------------------ - # Database queries: - /data/latest: - get: - tags: - - "Database queries" - summary: Fetched the last added entry from the Database and provides it via a JSON output - operationId: getLatestData - responses: - "200": - description: Succes - Successfully fetched the database - content: - application/json: - schema: - $ref: "#/components/schemas/ServerContainers" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "404": - description: Error - No entries found inside database - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /data/all: - get: - tags: - - "Database queries" - summary: Provides all database entries with an index starting from 0 - operationId: getAllData - responses: - "200": - description: Succes - Successfully fetched the database - content: - application/json: - schema: - $ref: "#/components/schemas/IndexedServerContainers" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "404": - description: Error - No entries found inside database - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /data/clear: - delete: - tags: - - "Database queries" - summary: Deletes all database entries - operationId: dataClear - responses: - "200": - description: Succes - Successfully cleared the database - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Successfully cleared the database" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - # ------------------------------ - # Configuration: - /api/hosts: - get: - tags: - - "Configuration" - summary: Retrieves the configured name of all added Hosts - operationId: getHosts - responses: - "200": - description: Succes - Successfully fetched all configured hosts - content: - application/json: - schema: - type: array - example: '[ "Host-1", "Host-2" ]' - - "400": - description: Error - No hosts defined, please add a host via /conf/addHost - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /api/host/{hostName}/stats: - get: - tags: - - "Configuration" - summary: Shows general information about the target host, like dockeer engine version - operationId: getHostInfo - parameters: - - name: hostName - in: path - description: Hostname of the target host - required: true - schema: - type: string - responses: - "200": - description: Succes - Successfully fetched info about target host - content: - application/json: - schema: - $ref: "#/components/schemas/HostInfo" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "404": - description: Error - No Host found - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /api/system: - get: - tags: - - "Configuration" - summary: Fetched the installation details of this DockStatAPI instance - operationId: getSystem - responses: - "200": - description: Succes - Fetched system configuration - content: - application/json: - schema: - type: object - properties: - installedAt: - type: string - format: date-time - example: "2024-12-25T19:20:02.418Z" - backendVersion: - type: string - example: "2.0.1" - inDocker: - type: boolean - example: false - installedBy: - type: string - example: "user" - platform: - type: string - example: "linux" - arch: - type: string - example: "x64" - "400": - description: Error - Received empty configuration - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /api/config: - get: - tags: - - "Configuration" - summary: Retrieves information about the configured hosts - operationId: getConfig - responses: - "200": - description: Succes - Fetched system configuration - content: - application/json: - schema: - type: object - properties: - hosts: - type: array - items: - type: object - properties: - name: - type: string - example: "Host-1" - url: - type: string - example: "192.168.2.12" - port: - type: string - example: "2375" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /api/frontend-config: - get: - tags: - - "Configuration" - summary: Fetches the "Frontend Configuration" => Used in the DockStat frontend - operationId: getFrontendConfig - responses: - "200": - description: Succes - Fetched "Frontend Configuration" - content: - application/json: - schema: - $ref: "#/components/schemas/FrontendConfig" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /api/current-schedule: - get: - tags: - - "Configuration" - summary: Shows the current configured schedule (for fetching data) in seconds - operationId: getSchedule - responses: - "200": - description: Succes - Fetched schedule - content: - application/json: - schema: - type: object - properties: - interval: - type: integer - example: 600 - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /api/status: - get: - tags: - - "Miscellaneous" - summary: Pings all hosts to check reachability - operationId: getStatus - responses: - "200": - description: Succes - Gathered Status - content: - application/json: - schema: - $ref: "#/components/schemas/ApiStatus" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /api/containers: - get: - tags: - - "Miscellaneous" - summary: Fetched all container data directly from the host without reading from the database - operationId: getContainers - responses: - "200": - description: Succes - Fetched all container statistics - content: - application/json: - schema: - $ref: "#/components/schemas/ServerContainers" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - # ------------------------------ - # High availability: - /ha/config: - get: - tags: - - "High availability" - summary: Get the current high availability config - operationId: getHaConfig - responses: - "200": - description: Succes - Fetched high availability config - content: - application/json: - schema: - $ref: "#/components/schemas/HaConfig" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - - /ha/sync: - post: - tags: - - "High availability" - deprecated: true - summary: This route is not deprecated, but only used by the high availability feature - operationId: syncHa - responses: - "200": - description: Succes - Synchronized successfully - "400": - description: Error - `files` object is missing or invalid - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - - /ha/prepare-sync: - get: - tags: - - "High availability" - deprecated: true - summary: This route is not deprecated, but only used by the high availability feature - operationId: syncPrepare - responses: - "200": - description: Succes - Prepared all files for syncing - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - - # ------------------------------ - # Notification Service: - /notification-service/get-template: - get: - tags: - - "Notification Service" - summary: Fetches the current template for the notification service - operationId: getNsTemplate - responses: - "200": - description: Success - Fetched notification template - content: - application/json: - schema: - $ref: "#/components/schemas/Notification-Template" - "400": - description: Error - Error while reading file (see server logs) - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /notification-service/set-template: - post: - tags: - - "Notification Service" - - "Configuration" - summary: Update the current notification template - operationId: setNsTemplate - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/Notification-Template" - responses: - "200": - description: Success - Template updated successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Template updated successfully." - "400": - description: Error - Invalid input format. Expected JSON with a 'text' field - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Invalid input format. Expected JSON with a 'text' field" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /notification-service/test/{type}/{containerId}: - post: - tags: - - "Notification Service" - summary: Test a specific type of notification using real data - operationId: testNs - parameters: - - in: path - name: type - required: true - schema: - type: string - description: The desired notification to test - - - in: path - name: containerId - required: true - schema: - type: string - description: A real container ID is needed to test templating functionality - responses: - "200": - description: Success - Sent test notification - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Sent test notification" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - # ------------------------------ - # Configuration: - /conf/addHost: - put: - tags: - - "Configuration" - summary: Adds a new host to the configuration and starts querying it - operationId: addHost - parameters: - - name: name - in: query - required: true - description: A name for the new host - - name: url - in: query - required: true - description: The target IP or dns entry - - name: port - in: query - required: true - description: The targets port on which Docker-Socket-Proxy runs - responses: - "200": - description: Success - Host added successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Host added successfully" - "400": - description: Error - Name, Port, and URL are required - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Name, Port, and URL are required" - "401": - description: Host already exists - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Host already exists" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /conf/removeHost: - delete: - tags: - - "Configuration" - summary: Removes an host from the config - operationId: removeHost - parameters: - - name: hostName - in: query - required: true - description: "The name of the to-be-removed-Host" - responses: - "200": - description: Success - Host removed successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Host removed successfully" - "401": - description: Error - Host name is required - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Host name is required" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "404": - description: Error - Host not found - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Host not found" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /conf/scheduler: - tags: - - "Configuration" - summary: Adjust the scheduler timing - operationId: adjustSchedule - parameters: - - name: interval - in: query - required: true - description: "Adjust the schedule timing (in seconds)" - responses: - "200": - description: Success - Timing updated - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Updated interval" - "401": - description: Error - Interval must be between 5 minutes and 6 hours - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Interval must be between 5 minutes and 6 hours." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - # ------------------------------ - # Frontend routes: - /frontend/show/{containerName}: - post: - tags: - - "Frontend Configuration" - operationId: frShowCon - summary: Set `hide` to false for the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to unhide - responses: - "200": - description: Success - now showing the container - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Container unhidden successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/hide/{containerName}: - delete: - tags: - - "Frontend Configuration" - operationId: frHideCon - summary: Set `hide` to true for the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to unhide - responses: - "200": - description: Success - now hiding the container - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Hid container succesfully" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/tag/{containerName}/{tag}: - post: - tags: - - "Frontend Configuration" - operationId: frTagCon - summary: Add a tag to the tag array for the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to add a tag to - - name: tag - in: path - schema: - type: string - required: true - description: The name of the tag to add - responses: - "200": - description: Success - Tag added successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Tag added successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/remove-tag/{containerName}/{tag}: - delete: - tags: - - "Frontend Configuration" - operationId: frRmTagCon - summary: Remove the specified tag from the tag array for the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to remove a tag from - - name: tag - in: path - schema: - type: string - required: true - description: The name of the tag to remove - responses: - "200": - description: Success - Tag removed successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Tag removed successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/pin/{containerName}: - post: - tags: - - "Frontend Configuration" - operationId: frPinCon - summary: Set `pinned` to true for the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to pin - responses: - "200": - description: Success - Container pinned successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Container pinned successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/unpin/{containerName}: - delete: - tags: - - "Frontend Configuration" - operationId: frRmPinCon - summary: Set `pinned` to false for the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to unpin - responses: - "200": - description: Success - Container unpinned successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Container unpinned successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/add-link/{containerName}/{link}: - post: - tags: - - "Frontend Configuration" - operationId: frAddLinkCon - summary: Add a link to the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to add a link to - - name: link - in: path - schema: - type: URI - required: true - allowReserved: false - description: The URI of the link (please use Uniform Resource Identifier format) - responses: - "200": - description: Success - Link added to container successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Link added successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/remove-link/{containerName}: - delete: - tags: - - "Frontend Configuration" - operationId: frRmLinkCon - summary: Remove a link to the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to remove a link from - responses: - "200": - description: Success - Link removed from container successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Link removed successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/add-icon/{containerName}/{icon}/{useCustomIcon}: - post: - tags: - - "Frontend Configuration" - operationId: frAddIcon - summary: Add an icon (path) to the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to add an icon to - - name: icon - in: path - schema: - type: string - required: true - description: The name of the icon file - - name: useCustomIcon - in: path - schema: - type: boolean - required: false - description: If the icon is a custom icon or not - responses: - "200": - description: Success - Icon added to container successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Icon added successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /frontend/remove-icon/{containerName}: - delete: - tags: - - "Frontend Configuration" - operationId: frRmIcon - summary: Remove an icon from the specified container - parameters: - - name: containerName - in: path - schema: - type: string - required: true - description: The name of the container to remove an icon from - responses: - "200": - description: Success - Icon removed from container successfully - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Icon removed successfully." - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - # ------------------------------ - # Stack management - /stacks/create/{name}: - post: - tags: - - "Stacks" - operationId: createStack - summary: Creates a docker-compose file inside the stack name directory - requestBody: - required: true - content: - application/json: - schema: - type: string - description: Your docker-compose.yaml contents - parameters: - - name: name - in: path - schema: - type: string - required: true - description: The name of the stack - responses: - "200": - description: Success - Stack created - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Stack created" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /stacks/start/{name}: - post: - tags: - - "Stacks" - operationId: startStack - summary: Starts the defined stack - parameters: - - name: name - in: path - schema: - type: string - required: true - description: The name of the stack - responses: - "200": - description: Success - Stack started - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Stack created" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /stacks/stop/{name}: - post: - tags: - - "Stacks" - operationId: stopStack - summary: Stops the defined stack - parameters: - - name: name - in: path - schema: - type: string - required: true - description: The name of the stack - responses: - "200": - description: Success - Stack stopped - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "success" - message: - type: string - example: "Stack stopped" - - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /stacks/get/{name}: - get: - tags: - - "Stacks" - operationId: getStack - summary: Get the docker-compose.yaml (as JSON) from the defined stack - parameters: - - name: name - in: path - schema: - type: string - required: true - description: The name of the stack - responses: - "200": - description: Success - Stack fetched - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /stacks/set-env/{name}: - post: - tags: - - "Stacks" - operationId: setStackEnv - summary: Set the docker.env (as JSON) from the defined stack - requestBody: - required: true - content: - application/json: - schema: - type: string - description: Your docker.env contents - parameters: - - name: override - in: query - required: false - description: Whether to override (true) the automatic environment file management (boolean value) - - name: name - in: path - schema: - type: string - required: true - description: The name of the stack - responses: - "200": - description: Success - Stack environment set - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - - /stacks/get-env/{name}: - get: - tags: - - "Stacks" - operationId: getStackEnv - summary: Get the docker.env (as JSON) from the defined stack - parameters: - - name: name - in: path - schema: - type: string - required: true - description: The name of the stack - responses: - "200": - description: Success - Stack config fetched - "403": - description: Error - Password is required - content: - application/json: - schema: - $ref: "#/components/schemas/403" - "500": - description: Error - Critical Error, please see the server's logs - content: - application/json: - schema: - $ref: "#/components/schemas/500" - "503": - description: Error - The high-availability lock is currently active, please try again later - content: - application/json: - schema: - $ref: "#/components/schemas/503" - -# ------------------------------ -components: - securitySchemes: - passwordAuth: - type: apiKey - in: header - name: x-password - description: Password required for authentication - - schemas: - Notification-Template: - type: object - properties: - text: - type: string - example: "{{container}} on {{host}} is {{state}}" - - IndexedServerContainers: - type: object - properties: - "0": - type: object - properties: - Host-1: - type: array - items: - $ref: "#/components/schemas/Container" - additionalProperties: false - - ServerContainers: - type: object - properties: - Host-1: - type: array - items: - $ref: "#/components/schemas/Container" - additionalProperties: false - - Container: - type: object - properties: - name: - type: string - description: The name of the container. - example: "Container-1" - id: - type: string - description: The unique identifier of the container. - example: "a84ca83bb0e7f8c24fe472b9164d40a4bae518ece8369e6776f722b81dd65bcf" - hostName: - type: string - description: The hostname of the server. - example: "Host-1" - state: - type: string - description: The current state of the container. - example: "running" - cpu_usage: - type: number - description: The CPU usage of the container in arbitrary units. - example: 625185.1851851852 - mem_usage: - type: integer - description: Memory usage in bytes. - example: 359899136 - mem_limit: - type: integer - description: Memory limit in bytes. - example: 8127893504 - net_rx: - type: integer - description: Total network received in bytes. - example: 11004185462 - net_tx: - type: integer - description: Total network transmitted in bytes. - example: 9950013623 - current_net_rx: - type: integer - description: Current network received in bytes. - example: 11004185462 - current_net_tx: - type: integer - description: Current network transmitted in bytes. - example: 9950013623 - networkMode: - type: string - description: The network mode of the container. - example: "docker_default" - - HostInfo: - type: object - properties: - hostName: - type: string - example: "Host-1" - info: - type: object - properties: - ID: - type: string - format: uuid - example: "32b5fad9-9b12-48b0-9ce7-178f2886ad60" - Containers: - type: integer - example: 8 - ContainersRunning: - type: integer - example: 8 - ContainersPaused: - type: integer - example: 0 - ContainersStopped: - type: integer - example: 0 - Images: - type: integer - example: 7 - OperatingSystem: - type: string - example: "Ubuntu 24.04 LTS" - KernelVersion: - type: string - example: "6.8.0-38-generic" - Architecture: - type: string - example: "x86_64" - MemTotal: - type: integer - example: 8127893504 - NCPU: - type: integer - example: 4 - version: - type: object - properties: - Components: - type: object - properties: - Engine: - type: string - example: "27.1.1" - containerd: - type: string - example: "1.7.19" - runc: - type: string - example: "1.7.19" - docker-init: - type: string - example: "0.19.0" - - Frontend: - type: object - properties: - name: - type: string - description: The name of the container - hidden: - type: boolean - description: Whether the container is hidden - tags: - type: array - items: - type: string - description: List of tags associated with the container - link: - type: string - format: uri - description: A link associated with the container - icon: - type: string - description: Icon for the container - pinned: - type: boolean - description: Whether the container is pinned - required: - - name - - FrontendConfig: - type: array - items: - $ref: "#/components/schemas/Frontend" - - ApiStatus: - type: object - properties: - ApiReachable: - type: boolean - description: Whether the API is reachable - online: - type: object - description: Status of individual services keyed by their names - properties: - Host-1: - type: boolean - Host-2: - type: boolean - required: - - ApiReachable - - online - - HaConfig: - type: object - properties: - active: - type: boolean - description: Whether High availability is active or nots - master: - type: boolean - description: Whether this node is the master node - nodes: - type: array - items: - type: string - format: hostname - description: List of nodes in the cluster, specified by hostname or IP with port - required: - - active - - master - - nodes - - 401: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Invalid password" - - 403: - type: object - properties: - status: - type: string - example: "denied" - message: - type: string - example: "Password required" - - 500: - type: object - properties: - status: - type: string - example: "critical" - message: - type: string - example: "Please see the server logs for more info" - - 503: - type: object - properties: - status: - type: string - example: "error" - message: - type: string - example: "Service unavailable. The high-availability lock is currently active. Please try again later." diff --git a/src/config/swaggerConfig.ts b/src/config/swaggerConfig.ts deleted file mode 100644 index 39c074a6..00000000 --- a/src/config/swaggerConfig.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { SwaggerOptions } from "swagger-ui-express"; -import { css } from "./swaggerTheme"; - -export const options: SwaggerOptions = { - swaggerOptions: { - tryItOutEnabled: true, - }, - customCss: css, - explorer: false, -}; diff --git a/src/config/swaggerTheme.ts b/src/config/swaggerTheme.ts deleted file mode 100644 index d8a879c9..00000000 --- a/src/config/swaggerTheme.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const css = ` - -.swagger-ui .topbar { - display: none -} -`; diff --git a/src/config/variables.ts b/src/config/variables.ts deleted file mode 100644 index 37c67a23..00000000 --- a/src/config/variables.ts +++ /dev/null @@ -1,26 +0,0 @@ -import vars from "../data/variables.json"; - -export const { - VERSION, - RUNNING_IN_DOCKER, - TRUSTED_PROXIES, - HA_MASTER, - HA_MASTER_IP, - HA_NODE, - HA_UNSAFE, - DISCORD_WEBHOOK_URL, - EMAIL_SENDER, - EMAIL_RECIPIENT, - EMAIL_PASSWORD, - EMAIL_SERVICE, - PUSHBULLET_ACCESS_TOKEN, - PUSHOVER_USER_KEY, - PUSHOVER_API_TOKEN, - SLACK_WEBHOOK_URL, - TELEGRAM_BOT_TOKEN, - TELEGRAM_CHAT_ID, - WHATSAPP_API_URL, - WHATSAPP_RECIPIENT, - AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT, - LOG_LEVEL, -} = vars; diff --git a/src/controllers/auth.ts b/src/controllers/auth.ts deleted file mode 100644 index 905e39c9..00000000 --- a/src/controllers/auth.ts +++ /dev/null @@ -1,64 +0,0 @@ -import fs from "fs/promises"; -import logger from "../utils/logger"; -const passwordFile: string = "./src/data/password.json"; -const passwordBool: string = "./src/data/usePassword.txt"; - -async function authEnabled(): Promise { - let isAuthEnabled: boolean = false; - let data: string = ""; - try { - data = await fs.readFile(passwordBool, "utf8"); - isAuthEnabled = data.trim() === "true"; - return isAuthEnabled; - } catch (error: unknown) { - logger.error("Error reading file: ", error as Error); - return isAuthEnabled; - } -} - -async function readPasswordFile() { - let data: string = ""; - try { - data = await fs.readFile(passwordFile, "utf8"); - return data; - } catch (error: unknown) { - logger.error("Could not read saved password: ", error as Error); - return data; - } -} - -async function writePasswordFile(passwordData: string) { - try { - await fs.writeFile(passwordFile, passwordData); - setTrue(); - logger.debug("Authentication enabled"); - return "Authentication enabled"; - } catch (error: unknown) { - logger.error("Error writing password file:", error as Error); - return error; - } -} - -async function setTrue() { - try { - await fs.writeFile(passwordBool, "true", "utf8"); - logger.info(`Enabled authentication`); - return; - } catch (error: unknown) { - logger.error("Error writing to the file:", error as Error); - return; - } -} - -async function setFalse() { - try { - await fs.writeFile(passwordBool, "false", "utf8"); - logger.info(`Disabled authentication`); - return; - } catch (error: unknown) { - logger.error("Error writing to the file:", error as Error); - return; - } -} - -export { authEnabled, readPasswordFile, writePasswordFile, setFalse }; diff --git a/src/controllers/containerController.ts b/src/controllers/containerController.ts deleted file mode 100644 index 2883dad9..00000000 --- a/src/controllers/containerController.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { getDockerClient } from "../utils/dockerClient"; -import logger from "../utils/logger"; -import { Request, Response } from "express"; -import { createResponseHandler } from "../handlers/response"; - -const getContainers = async (req: Request, res: Response): Promise => { - const ResponseHandler = createResponseHandler(res); - const host: string = (req.query.host as string) || "local"; - - logger.info(`Fetching containers from host: ${host}`); - - try { - const docker = getDockerClient(host); - const containers = await docker.listContainers(); - - return ResponseHandler.rawData( - containers, - `Fetched containers from ${host}`, - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } -}; - -const getContainerStats = async ( - containerID: string, - containerHost: string, - res: Response, -): Promise => { - logger.info( - `Fetching stats for container: ${containerID} from host: ${containerHost}`, - ); - const ResponseHandler = createResponseHandler(res); - - try { - const docker = getDockerClient(containerHost); - const container = docker.getContainer(containerID); - const stats = await container.stats({ stream: false }); - - return ResponseHandler.rawData( - stats, - `Successfully fetched stats for container: ${containerID} from host: ${containerHost}`, - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } -}; - -export default { - getContainers, - getContainerStats, -}; diff --git a/src/controllers/databaseMigration.ts b/src/controllers/databaseMigration.ts deleted file mode 100644 index 45f88d12..00000000 --- a/src/controllers/databaseMigration.ts +++ /dev/null @@ -1,20 +0,0 @@ -import db from "../config/db"; -import logger from "../utils/logger"; - -function clearOldEntries(): void { - const twentyFourHoursAgo: number = Date.now() - 24 * 60 * 60 * 1000; - - db.run( - `DELETE FROM data WHERE createdAt < ?`, - [twentyFourHoursAgo], - (err: Error | null) => { - if (err) { - logger.error("Error deleting old entries:", err.message); - throw new Error("Database cleanup failed"); - } - logger.info("Old entries cleared successfully"); - }, - ); -} - -export default clearOldEntries; diff --git a/src/controllers/fetchData.ts b/src/controllers/fetchData.ts deleted file mode 100644 index 06e52a93..00000000 --- a/src/controllers/fetchData.ts +++ /dev/null @@ -1,76 +0,0 @@ -import db from "../config/db"; -import { fetchAllContainers } from "../utils/containerService"; -import logger from "../utils/logger"; -import fs from "fs"; -import { atomicWrite } from "../utils/atomicWrite"; -const filePath = "./src/data/states.json"; - -let previousState: { [key: string]: string } = {}; - -interface Container { - name: string; - id: string; - state: string; - hostName: string; -} - -interface AllContainerData { - [host: string]: Container[] | { error: string }; -} - -const fetchData = async (): Promise => { - try { - const allContainerData: AllContainerData = - (await fetchAllContainers()) || {}; - - db.run( - `INSERT INTO data (info) VALUES (?)`, - [JSON.stringify(allContainerData)], - function (error) { - if (error) { - logger.error("Error inserting data:", error); - return; - } - logger.info(`Data inserted with ID: ${this.lastID}`); - }, - ); - - const containerStatus: AllContainerData = {}; - - Object.keys(allContainerData).forEach((host) => { - const containers = allContainerData[host]; - - // Handle if the containers are an array, otherwise handle the error - if (Array.isArray(containers)) { - containerStatus[host] = containers.map((container: Container) => ({ - name: container.name, - id: container.id, - state: container.state, - hostName: container.hostName, - })); - } else { - // If there's an error, handle it separately - containerStatus[host] = { error: "Error fetching containers" }; - } - }); - - if (fs.existsSync(filePath)) { - const fileData = fs.readFileSync(filePath, "utf8"); - previousState = fileData ? JSON.parse(fileData) : {}; - } - - // Compare previous and current state - if (JSON.stringify(previousState) !== JSON.stringify(containerStatus)) { - atomicWrite(filePath, JSON.stringify(containerStatus, null, 2)); - logger.info(`Container states saved to ${filePath}`); - // TODO: Add logic + notification levels per service - } else { - logger.info("No state change detected, notifications not triggered."); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -}; - -export default fetchData; diff --git a/src/controllers/frontendConfiguration.ts b/src/controllers/frontendConfiguration.ts deleted file mode 100644 index ed4e59dd..00000000 --- a/src/controllers/frontendConfiguration.ts +++ /dev/null @@ -1,297 +0,0 @@ -import fs from "fs"; -import logger from "../utils/logger"; -const dataPath: string = "./src/data/frontendConfiguration.json"; -const expression: string = - "https?://(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}([-a-zA-Z0-9()@:%_+.~#?&//=]*)"; -const regex = new RegExp(expression); -import { FrontendConfig } from "../typings/frontendConfig"; - -/////////////////////////////////////////////////////////////// -// Hide Containers: -async function hideContainer(containerName: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - data[containerIndex].hidden = true; - await saveData(data); - } else { - data.push({ name: containerName, hidden: true }); - await saveData(data); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -async function unhideContainer(containerName: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - delete data[containerIndex].hidden; - await saveData(data); - cleanupData(); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -/////////////////////////////////////////////////////////////// -// Tag containers -async function addTagToContainer(containerName: string, tag: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - if (!data[containerIndex].tags) { - data[containerIndex].tags = []; - } - data[containerIndex].tags.push(tag); - await saveData(data); - } else { - data.push({ name: containerName, tags: [tag] }); - await saveData(data); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -async function removeTagFromContainer(containerName: string, tag: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1 && data[containerIndex].tags) { - data[containerIndex].tags = data[containerIndex].tags.filter( - (t) => t !== tag, - ); - await saveData(data); - cleanupData(); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -/////////////////////////////////////////////////////////////// -// Pin containers -async function pinContainer(containerName: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - data[containerIndex].pinned = true; - await saveData(data); - } else { - data.push({ name: containerName, pinned: true }); - await saveData(data); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -async function unpinContainer(containerName: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - delete data[containerIndex].pinned; - await saveData(data); - cleanupData(); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -/////////////////////////////////////////////////////////////// -// Add/remove link from containers -async function setLink(containerName: string, link: string) { - if (link.match(regex)) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - data[containerIndex].link = `${link}`; - await saveData(data); - } else { - data.push({ name: containerName, link: `${link}` }); - await saveData(data); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } - } else { - logger.error(`Provided link is not valid: ${link}`); - throw new Error(`Provided link is not valid: ${link}`); - } -} - -async function removeLink(containerName: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - delete data[containerIndex].link; - await saveData(data); - cleanupData(); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -/////////////////////////////////////////////////////////////// -// Add/remove icon from containers -async function setIcon(containerName: string, icon: string, custom: boolean) { - try { - const data = await readData(); - const containerIndex: number = data.findIndex( - (container) => container.name === containerName, - ); - - if (custom === true) { - if (containerIndex !== -1) { - data[containerIndex].icon = `custom/${icon}`; - await saveData(data); - } else { - data.push({ name: containerName, icon: `custom/${icon}` }); - await saveData(data); - } - } else if (containerIndex !== -1) { - data[containerIndex].icon = `${icon}`; - await saveData(data); - } else { - data.push({ name: containerName, icon: `${icon}` }); - await saveData(data); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -async function removeIcon(containerName: string) { - try { - const data = await readData(); - const containerIndex = data.findIndex( - (container) => container.name === containerName, - ); - - if (containerIndex !== -1) { - delete data[containerIndex].icon; - await saveData(data); - cleanupData(); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -/////////////////////////////////////////////////////////////// -// Data specific functionss -async function readData() { - try { - const data: FrontendConfig = JSON.parse( - await fs.promises.readFile(dataPath, "utf-8"), - ); - return data; - } catch (error: unknown) { - console.error(`Error while reading ${dataPath}: ${error as Error}`); - if (error as Error) { - await saveData([]); - return []; - } else { - throw error; - } - } -} - -async function saveData(data: FrontendConfig) { - try { - await fs.promises.writeFile( - dataPath, - JSON.stringify(data, null, 2), - "utf-8", - ); - logger.info("Succesfully wrote to file"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -async function cleanupData() { - try { - const data = await readData(); - let cleanedData: FrontendConfig = []; - - if (data && Array.isArray(data)) { - cleanedData = data.filter((container) => { - // Filter out containers with empty "tags" or containers with only one property (name) - if ( - container.tags && - Array.isArray(container.tags) && - container.tags.length === 0 - ) { - delete container.tags; - } - return Object.keys(container).length > 1; - }); - } - - await saveData(cleanedData); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -export { - hideContainer, - unhideContainer, - addTagToContainer, - removeTagFromContainer, - pinContainer, - unpinContainer, - setLink, - removeLink, - setIcon, - removeIcon, -}; diff --git a/src/controllers/highAvailability.ts b/src/controllers/highAvailability.ts deleted file mode 100644 index 45db9d7b..00000000 --- a/src/controllers/highAvailability.ts +++ /dev/null @@ -1,285 +0,0 @@ -import logger from "../utils/logger"; -import fs from "fs"; -import chokidar from "chokidar"; -import path from "path"; -import { promisify } from "util"; -import { - HA_UNSAFE, - HA_MASTER, - HA_MASTER_IP, - HA_NODE, -} from "../config/variables"; -import { atomicWrite } from "../utils/atomicWrite"; -import { HighAvailabilityConfig, HaNodeConfig, NodeCache } from "../typings/ha"; - -const sleep = promisify(setTimeout); - -const haMasterPath: string = "./src/data/highAvailability.json"; -const haNodePath: string = "./src/data/haNode.json"; -const nodeCachePath: string = "./src/data/nodeCache.json"; -const useUnsafeConnection: boolean = JSON.parse(HA_UNSAFE || "false"); -const lockFilePath: string = "./src/data/ha.lock"; - -const configFiles: string[] = [ - "./src/data/dockerConfig.json", - "./src/data/states.json", - "./src/data/template.json", - "./src/data/frontendConfiguration.json", - "./src/data/nodeCache.json", - "./src/data/usePassword.txt", - "./src/data/password.json", -]; - -const MAX_RETRIES = 10; -const BASE_DELAY_MS = 100; - -async function acquireLock(): Promise { - let retryCount = 0; - - while (fs.existsSync(lockFilePath)) { - if (retryCount >= MAX_RETRIES) { - throw new Error( - "Failed to acquire lock: maximum retry attempts exceeded", - ); - } - - const backoffMs = BASE_DELAY_MS * Math.pow(2, retryCount); - const jitter = Math.random() * 0.3 * backoffMs; - const delayMs = backoffMs + jitter; - - logger.warn( - `Lock file exists, waiting ${Math.round(delayMs)}ms before retry ${retryCount + 1}/${MAX_RETRIES}...`, - ); - await sleep(delayMs); - retryCount++; - } - - try { - atomicWrite(lockFilePath, "locked", { exclusive: true }); - logger.debug("Lock acquired."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -async function releaseLock(): Promise { - try { - if (fs.existsSync(lockFilePath)) { - await fs.promises.unlink(lockFilePath); - logger.debug("Lock released."); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -async function writeConfig( - data: HighAvailabilityConfig | NodeCache | HaNodeConfig, - filePath: string, -): Promise { - await acquireLock(); - try { - logger.debug(`Writing ${filePath}`); - const dirPath: string = path.dirname(filePath); - await fs.promises.mkdir(dirPath, { recursive: true }); - - const jsonData = JSON.stringify(data, null, 2); - await fs.promises.writeFile(filePath, jsonData); - - logger.debug(`${filePath} has been written.`); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } finally { - await releaseLock(); - } -} - -async function readConfig(): Promise { - await acquireLock(); - try { - logger.debug("Reading HA-Config"); - const data: HighAvailabilityConfig = JSON.parse( - fs.readFileSync(haMasterPath, "utf-8"), - ); - return data; - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return null; - } finally { - await releaseLock(); - } -} - -async function prepareFilesForSync(): Promise> { - const fileData: Record = {}; - try { - for (const filePath of configFiles) { - const content = await fs.promises.readFile(filePath, "utf-8"); - fileData[filePath] = content; - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } - return fileData; -} - -async function checkApiReachable(node: string): Promise { - const nodeUrl = - useUnsafeConnection === true - ? `http://${node}/api/status` - : `https://${node}/api/status`; - - logger.info(`Checking node (${nodeUrl}) reachability`); - - try { - const response = await fetch(nodeUrl); - if (!response.ok) { - logger.error(`Failed to reach node ${node}. Status: ${response.status}`); - return false; - } - - const data = await response.json(); - if (data.ApiReachable as boolean) { - logger.info(`Node ${node} is reachable.`); - return true; - } else { - logger.error(`Node ${node} is not reachable. ApiReachable: false`); - return false; - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return false; - } -} - -async function synchronizeFilesWithNodes(): Promise { - const haConfig = await readConfig(); - - if (!haConfig || !haConfig.master || haConfig.nodes.length === 0) { - logger.warn("No slave nodes to synchronize with."); - return; - } - - const files = await prepareFilesForSync(); - - for (const node of haConfig.nodes) { - if (!(await checkApiReachable(node))) { - logger.warn( - `Skipping file sync with ${node} due to connectivity issues.`, - ); - continue; // Skip synchronization if the node is unreachable - } - - const nodeUrl = - useUnsafeConnection === true - ? `http://${node}/ha/sync` - : `https://${node}/ha/sync`; - - logger.info(`Synchronizing files with node: ${node}`); - - const response = await fetch(nodeUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ files }), - }); - - if (response.ok) { - logger.info(`Files synchronized successfully with node: ${node}`); - } else { - logger.error( - `Failed to synchronize files with node ${node}. Status: ${response.status}`, - ); - } - } -} - -function monitorConfigFiles(): void { - const watcher = chokidar.watch(configFiles, { persistent: true }); - - watcher - .on("change", async (filePath) => { - logger.info(`File changed: ${filePath}. Initiating synchronization.`); - await synchronizeFilesWithNodes(); - }) - .on("error", (error) => { - logger.error(`Error watching files: ${(error as Error).message}`); - }); - - logger.info("Started monitoring configuration files for changes."); -} - -async function startMasterNode() { - if (HA_MASTER == "true") { - if (!HA_MASTER_IP) { - logger.error( - "Master's IP is not set, please set the HA_MASTER_IP variable (example: 10.0.0.4:9876)", - ); - } else { - const haNodeConfig: HaNodeConfig = { - master: HA_MASTER_IP, - }; - const haConfig: HighAvailabilityConfig = { - active: true, - master: true, - nodes: HA_NODE ? HA_NODE.split(",").map((node) => node.trim()) : [], - }; - - const nodeCache: NodeCache = HA_NODE - ? HA_NODE.split(",").reduce((cache, node, index) => { - const [ip, port] = node.trim().split(":"); - if (ip && port) { - cache[`node-${index + 1}`] = { ip, port: parseInt(port, 10) }; - } - return cache; - }, {} as NodeCache) - : {}; - - logger.debug("Writing HA-Config(s)"); - await writeConfig(haConfig, haMasterPath); - await writeConfig(haNodeConfig, haNodePath); - await writeConfig(nodeCache, nodeCachePath); - - logger.info("Running startup sync..."); - await synchronizeFilesWithNodes(); - logger.info("Watching config files in ./data"); - monitorConfigFiles(); - } - } else { - logger.info("This is a slave node"); - } -} - -async function ensureFileExists( - filePath: string, - content: string, -): Promise { - await acquireLock(); - try { - const dirPath = path.dirname(filePath); - await fs.promises.mkdir(dirPath, { recursive: true }); - await fs.promises.writeFile(filePath, content, { flag: "w" }); - logger.info(`File updated: ${filePath}`); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } finally { - await releaseLock(); - } -} - -export { - HighAvailabilityConfig, - writeConfig, - readConfig, - prepareFilesForSync, - synchronizeFilesWithNodes, - monitorConfigFiles, - startMasterNode, - ensureFileExists, -}; diff --git a/src/controllers/notificationController.ts b/src/controllers/notificationController.ts deleted file mode 100644 index 0ece9553..00000000 --- a/src/controllers/notificationController.ts +++ /dev/null @@ -1,60 +0,0 @@ -import notify from "../utils/notifications/_notify"; -import logger from "../utils/logger"; -import { - DISCORD_WEBHOOK_URL, - EMAIL_SENDER, - EMAIL_RECIPIENT, - EMAIL_PASSWORD, - EMAIL_SERVICE, - PUSHBULLET_ACCESS_TOKEN, - PUSHOVER_USER_KEY, - PUSHOVER_API_TOKEN, - SLACK_WEBHOOK_URL, - TELEGRAM_BOT_TOKEN, - TELEGRAM_CHAT_ID, - WHATSAPP_API_URL, - WHATSAPP_RECIPIENT, -} from "../config/variables"; - -const notificationTypes = { - discord: !!DISCORD_WEBHOOK_URL, - email: !!(EMAIL_SENDER && EMAIL_RECIPIENT && EMAIL_PASSWORD && EMAIL_SERVICE), - pushbullet: !!PUSHBULLET_ACCESS_TOKEN, - pushover: !!(PUSHOVER_API_TOKEN && PUSHOVER_USER_KEY), - slack: !!SLACK_WEBHOOK_URL, - telegram: !!(TELEGRAM_BOT_TOKEN && TELEGRAM_CHAT_ID), - whatsapp: !!(WHATSAPP_API_URL && WHATSAPP_RECIPIENT), -}; - -async function sendNotification(containerId: string) { - if (notificationTypes.discord) { - logger.debug(`Sending notification via discord (${containerId})`); - notify("discord", containerId); - } - if (notificationTypes.email) { - logger.debug(`Sending notification via E-Mail (${containerId})`); - notify("email", containerId); - } - if (notificationTypes.pushbullet) { - logger.debug(`Sending notification via Pushbullet (${containerId})`); - notify("pushbullet", containerId); - } - if (notificationTypes.pushover) { - logger.debug(`Sending notification via Pushover (${containerId})`); - notify("pushover", containerId); - } - if (notificationTypes.slack) { - logger.debug(`Sending notification via Slack (${containerId})`); - notify("slack", containerId); - } - if (notificationTypes.telegram) { - logger.debug(`Sending notification via Telegram (${containerId})`); - notify("slack", containerId); - } - if (notificationTypes.whatsapp) { - logger.debug(`Sending notification via Pushbullet (${containerId})`); - notify("whatsapp", containerId); - } -} - -export default sendNotification; diff --git a/src/controllers/proxy.ts b/src/controllers/proxy.ts deleted file mode 100644 index c091590a..00000000 --- a/src/controllers/proxy.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Application } from "express"; -import logger from "../utils/logger"; -import { TRUSTED_PROXIES } from "../config/variables"; - -export default function trustedProxies(app: Application) { - const trusted: string = TRUSTED_PROXIES; - - if (!trusted) { - logger.warn( - "No trusted Proxy configured, if ran behind a proxy please configure it according to the docs", - ); - } else { - app.set("trust proxy", trusted); - } -} diff --git a/src/controllers/scheduler.ts b/src/controllers/scheduler.ts deleted file mode 100644 index db450d95..00000000 --- a/src/controllers/scheduler.ts +++ /dev/null @@ -1,91 +0,0 @@ -import fetchData from "./fetchData"; -import logger from "../utils/logger"; -import db from "../config/db"; -const regex = /(\d{1,5})([smh])/g; - -let fetchInterval = 5 * 60 * 1000; // Fetch data every 5 minutes by default -const cleanupInterval = 24 * 60 * 60 * 1000; // every 24hrs -let intervalId: NodeJS.Timeout; - -const scheduleFetch = () => { - try { - fetchData(); - cleanupOldEntries(); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } - - intervalId = setInterval(() => { - logger.info( - `Fetching data at interval of ${fetchInterval / 1000} seconds.`, - ); - fetchData(); - }, fetchInterval); - - setInterval(() => { - cleanupOldEntries(); - }, cleanupInterval); - - logger.info(`Data fetching scheduled every ${fetchInterval / 1000} seconds.`); - logger.info("Old entries cleanup scheduled every 24 hours."); - - // Additional 20-second interval to log process exit listeners, if any - setInterval(() => { - const exitListeners = process.listeners("exit"); - - if (exitListeners.length > 0) { - logger.info(`Exit listeners detected: ${exitListeners}`); - } - }, 20000); -}; - -const setFetchInterval = (newInterval: number) => { - if (intervalId) { - clearInterval(intervalId); - logger.info("Cleared existing fetch interval."); - } - fetchInterval = newInterval; - scheduleFetch(); - logger.info(`Fetch interval updated to ${fetchInterval / 1000} seconds.`); -}; - -const parseInterval = (interval: string) => { - const timeUnits: { [key: string]: number } = { - s: 1000, - m: 60 * 1000, - h: 60 * 60 * 1000, - }; - - let totalMilliseconds = 0; - let match; - - while ((match = regex.exec(interval))) { - const value = parseInt(match[1], 10); - const unit = match[2]; - totalMilliseconds += value * timeUnits[unit]; - } - - return totalMilliseconds; -}; - -const getCurrentSchedule = () => { - return { - interval: fetchInterval / 1000, - }; -}; - -const cleanupOldEntries = async () => { - const twentyFourHoursAgo = new Date( - Date.now() - 24 * 60 * 60 * 1000, - ).toISOString(); - try { - db.run("DELETE FROM data WHERE timestamp < ?", twentyFourHoursAgo, Error); - logger.info("Old entries cleared from the database."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -}; - -export { scheduleFetch, setFetchInterval, parseInterval, getCurrentSchedule }; diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts new file mode 100644 index 00000000..6d5ea554 --- /dev/null +++ b/src/core/database/repository.ts @@ -0,0 +1,74 @@ +import Database from "bun:sqlite"; + +const db = new Database("dockstatapi.db"); + +export const dbFunctions = { + init() { + db.exec(` + CREATE TABLE IF NOT EXISTS docker_hosts ( + id TEXT PRIMARY KEY, + name TEXT, + url TEXT, + poll_interval INTEGER + ); + + CREATE TABLE IF NOT EXISTS container_metrics ( + host_id TEXT, + container_id TEXT, + cpu REAL, + memory REAL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS backend_log_entries ( + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + level TEXT, + message TEXT, + file TEXT, + line NUMBER + ); + `); + }, + + insertMetric(hostId: string, metric: any) { + const stmt = db.prepare(` + INSERT INTO container_metrics (host_id, container_id, cpu, memory) + VALUES (?, ?, ?, ?) + `); + return stmt.run(hostId, metric.containerId, metric.cpu, metric.memory); + }, + + addLogEntry: ( + level: string, + message: string, + file_name: string, + line: number, + ) => { + const stmt = db.prepare(` + INSERT INTO backend_log_entries (level, message, file, line) + VALUES (?, ?, ?, ?) + `); + return stmt.run(level, message, file_name, line); + }, + + getAllLogs() { + const stmt = db.prepare(` + SELECT timestamp, level, message, file, line + FROM backend_log_entries + ORDER BY timestamp DESC + `); + return stmt.all(); + }, + + getLogsByLevel(level: string) { + const stmt = db.prepare(` + SELECT timestamp, level, message, file, line + FROM backend_log_entries + WHERE level = ? + ORDER BY timestamp DESC + `); + return stmt.all(level); + }, +}; + +dbFunctions.init(); diff --git a/src/core/docker/host-manager.ts b/src/core/docker/host-manager.ts new file mode 100644 index 00000000..e2c1ccc4 --- /dev/null +++ b/src/core/docker/host-manager.ts @@ -0,0 +1,38 @@ +import WebSocket from "ws"; +import { pluginManager } from "~/core/plugins/plugin-manager"; +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; + +export class DockerHostManager { + public connections = new Map(); + + async connect(hostId: string, url: string) { + const ws = new WebSocket(url); + + ws.on("open", () => { + this.connections.set(hostId, ws); + logger.info(`Opened connection to ${hostId}`); + }); + + ws.on("message", (data) => { + this.handleData(hostId, JSON.parse(data.toString())); + }); + + ws.on("close", () => { + this.connections.delete(hostId); + logger.info(`Disconnected from Docker host ${hostId}`); + }); + } + + private handleData(hostId: string, data: any) { + dbFunctions.insertMetric(hostId, data); + + if (data.event === "container_start") { + pluginManager.handleContainerStart(data.container); + } + + pluginManager.handleMetrics(data); + } +} + +export const dockerHostManager = new DockerHostManager(); diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts new file mode 100644 index 00000000..40f79c4d --- /dev/null +++ b/src/core/plugins/loader.ts @@ -0,0 +1,21 @@ +import { pluginManager } from "./plugin-manager"; +import path from "path"; +import fs from "fs"; + +export async function loadPlugins(pluginDir: string) { + const pluginPath = path.join(process.cwd(), pluginDir); + + if (!fs.existsSync(pluginPath)) { + return; + } + + const files = fs.readdirSync(pluginPath); + + for (const file of files) { + if (!file.endsWith(".plugin.ts")) continue; + + const module = await import(path.join(pluginPath, file)); + const plugin = module.default; + pluginManager.register(plugin); + } +} diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts new file mode 100644 index 00000000..15d66f45 --- /dev/null +++ b/src/core/plugins/plugin-manager.ts @@ -0,0 +1,35 @@ +import { EventEmitter } from "events"; + +export interface Plugin { + name: string; + onContainerStart?: (containerInfo: any) => void; + onMetricsReceived?: (metrics: any) => void; + onLogReceived?: (log: string) => void; +} + +export class PluginManager extends EventEmitter { + private plugins: Map = new Map(); + + register(plugin: Plugin) { + this.plugins.set(plugin.name, plugin); + console.log(`Registered plugin: ${plugin.name}`); + } + + unregister(name: string) { + this.plugins.delete(name); + } + + handleContainerStart(containerInfo: any) { + this.plugins.forEach((plugin) => { + plugin.onContainerStart?.(containerInfo); + }); + } + + handleMetrics(metrics: any) { + this.plugins.forEach((plugin) => { + plugin.onMetricsReceived?.(metrics); + }); + } +} + +export const pluginManager = new PluginManager(); diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts new file mode 100644 index 00000000..076e3857 --- /dev/null +++ b/src/core/utils/logger.ts @@ -0,0 +1,72 @@ +import { createLogger, format, transports } from "winston"; +import Transport from "winston-transport"; +import path from "path"; +import { dbFunctions } from "../database/repository"; +import chalk from "chalk"; + +const fileLineFormat = format((info) => { + try { + const stack = new Error().stack?.split("\n"); + if (stack) { + for (let i = 2; i < stack.length; i++) { + const line = stack[i].trim(); + if ( + !line.includes("node_modules") && + !line.includes(path.basename(__filename)) + ) { + const matches = line.match(/\(?(.+):(\d+):(\d+)\)?$/); + if (matches) { + info.file = path.basename(matches[1]); + info.line = parseInt(matches[2]); + break; + } + } + } + } + } catch (err) { + // Ignore errors in case stack trace parsing fails + } + return info; +}); + +class SQLiteTransport extends Transport { + constructor(opts?: Transport.TransportStreamOptions) { + super(opts); + } + + log(info: any, callback: () => void) { + const { level, message, file, line } = info; + dbFunctions.addLogEntry(level, message, file || "unknown", line || 0); + callback(); + } +} + +export const logger = createLogger({ + level: "debug", + format: format.combine(fileLineFormat(), format.json()), + transports: [ + new transports.Console({ + format: format.combine( + format.printf(({ level, message, file, line }) => { + const levelColors: { [key: string]: chalk.Chalk } = { + error: chalk.red.bold, + warn: chalk.yellow.bold, + info: chalk.green.bold, + debug: chalk.blue.bold, + verbose: chalk.cyan.bold, + silly: chalk.magenta.bold, + }; + + const paddedLevel = level.padEnd(5).toUpperCase(); + const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); + + const coloredContext = chalk.cyan(`${file}:${line}`); + const coloredMessage = chalk.gray(message); + + return `[ ${coloredContext.padEnd(22)} ] ${coloredLevel} - ${coloredMessage}`; + }), + ), + }), + new SQLiteTransport(), + ], +}); diff --git a/src/data/frontendConfiguration.json b/src/data/frontendConfiguration.json deleted file mode 100644 index 0637a088..00000000 --- a/src/data/frontendConfiguration.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/src/data/template.json b/src/data/template.json deleted file mode 100644 index 75e12f22..00000000 --- a/src/data/template.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "text": "{{name}} is {{state}} on {{hostName}}" -} diff --git a/src/data/usePassword.txt b/src/data/usePassword.txt deleted file mode 100644 index 02e4a84d..00000000 --- a/src/data/usePassword.txt +++ /dev/null @@ -1 +0,0 @@ -false \ No newline at end of file diff --git a/src/handlers/api.ts b/src/handlers/api.ts deleted file mode 100644 index fa7f1f7e..00000000 --- a/src/handlers/api.ts +++ /dev/null @@ -1,142 +0,0 @@ -import extractRelevantData from "../utils/extractHostData"; -import { Request, Response } from "express"; -import { getDockerClient } from "../utils/dockerClient"; -import { fetchAllContainers } from "../utils/containerService"; -import { getCurrentSchedule } from "../controllers/scheduler"; -import fs from "fs"; -import checkReachability from "../utils/connectionChecker"; -const configPath = "./src/data/dockerConfig.json"; -const userConf = "./src/data/user.conf"; -import { dockerConfig } from "../typings/dockerConfig"; -import { createResponseHandler } from "./response"; - -class ApiHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - hosts() { - const ResponseHandler = createResponseHandler(this.res); - try { - const rawData = fs.readFileSync(configPath, "utf-8"); - const config: dockerConfig = JSON.parse(rawData); - - if (!config.hosts) { - return ResponseHandler.error("No hosts defined in configuration.", 400); - } - - const hosts = config.hosts.map((host) => host.name); - return ResponseHandler.rawData(hosts, "Fetched data for all hosts"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - system() { - const ResponseHandler = createResponseHandler(this.res); - try { - const rawData = fs.readFileSync(userConf, "utf8"); - const config = JSON.parse(rawData); - - if (!config) { - return ResponseHandler.error("Received empty configuration", 400); - } - - return ResponseHandler.rawData(config, "Fetched system configuration"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async hostStats(hostName: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - const docker = getDockerClient(hostName); - const info = await docker.info(); - const version = await docker.version(); - const relevantData = extractRelevantData({ hostName, info, version }); - - if (!relevantData) { - ResponseHandler.error("No host found", 404); - } - - return ResponseHandler.rawData(relevantData, "Fetched Host stats"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async containers() { - const ResponseHandler = createResponseHandler(this.res); - try { - const allContainerData = await fetchAllContainers(); - return ResponseHandler.rawData( - allContainerData, - "Fetched all containers across all hosts", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async config() { - const ResponseHandler = createResponseHandler(this.res); - try { - const rawData = fs.readFileSync(configPath); - const data = JSON.parse(rawData.toString()); - return ResponseHandler.rawData(data, "Fetched config"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - currentSchedule() { - const ResponseHandler = createResponseHandler(this.res); - try { - const currentSchedule = getCurrentSchedule(); - return ResponseHandler.rawData( - currentSchedule, - "Fetched current schedule", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async status() { - const ResponseHandler = createResponseHandler(this.res); - try { - const data = await checkReachability(); - return ResponseHandler.rawData(data, "Fetched Status"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - frontendConfig() { - const configPath: string = "./src/data/frontendConfiguration.json"; - const ResponseHandler = createResponseHandler(this.res); - try { - const rawData = fs.readFileSync(configPath); - const data = JSON.parse(rawData.toString()); - return ResponseHandler.rawData(data, "Fetched frontend configuration"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } -} - -export const createApiHandler = (req: Request, res: Response) => - new ApiHandler(req, res); diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts deleted file mode 100644 index 4dfbd3fb..00000000 --- a/src/handlers/auth.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Request, Response } from "express"; -import { - authEnabled, - readPasswordFile, - writePasswordFile, - setFalse, -} from "../controllers/auth"; -import { createResponseHandler } from "./response"; -import bcrypt from "bcrypt"; - -const saltRounds: number = 10; - -class AuthenticationHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - async enable(password: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - if (await authEnabled()) { - return ResponseHandler.denied( - "Password Authentication is already enabled, please deactivate it first", - ); - } - - if (!password) { - return ResponseHandler.denied("Password is required"); - } - - const salt = await bcrypt.genSalt(saltRounds); - const hash = await bcrypt.hash(password, salt); - - const passwordData = { hash, salt }; - writePasswordFile(JSON.stringify(passwordData)); - - return ResponseHandler.ok("Authentication enabled successfully"); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async disable(password: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - if (!password) { - return ResponseHandler.denied("Password is required"); - } - - const storedData = JSON.parse(await readPasswordFile()); - const isPasswordValid = await bcrypt.compare(password, storedData.hash); - - if (!isPasswordValid) { - return ResponseHandler.error("Invalid password", 401); - } - - await setFalse(); - return ResponseHandler.ok("Authentication disabled successfully"); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } -} - -export const createAuthenticationHandler = (req: Request, res: Response) => - new AuthenticationHandler(req, res); diff --git a/src/handlers/conf.ts b/src/handlers/conf.ts deleted file mode 100644 index b49dd2a5..00000000 --- a/src/handlers/conf.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { setFetchInterval, parseInterval } from "../controllers/scheduler"; -import { Request, Response } from "express"; -import fs from "fs"; -import { createResponseHandler } from "./response"; -import { target, dockerConfig } from "../typings/dockerConfig"; -const configPath: string = "./src/data/dockerConfig.json"; - -class ConfHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - addHost(req: Request) { - const ResponseHandler = createResponseHandler(this.res); - - try { - const { name, url, port } = req.query as unknown as target; - if (!name || !url || !port) { - return ResponseHandler.error("Name, Port, and URL are required.", 400); - } - - const config: dockerConfig = JSON.parse( - fs.readFileSync(configPath, "utf-8"), - ); - - if (config.hosts.some((host) => host.name === name)) { - return ResponseHandler.error("Host already exists.", 422); - } - - config.hosts.push({ name, url, port }); - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); - - return ResponseHandler.ok("Host added successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - removeHost(req: Request) { - const ResponseHandler = createResponseHandler(this.res); - try { - const hostName = req.query.hostName as string; - - if (!hostName) { - return ResponseHandler.error("Host name is required.", 401); - } - - const currentState = fs.readFileSync(configPath, "utf-8"); - const config: dockerConfig = JSON.parse(currentState); - - const hostIndex = config.hosts.findIndex( - (host) => host.name === hostName, - ); - - if (hostIndex === -1) { - return ResponseHandler.error("Host not found.", 404); - } - - config.hosts.splice(hostIndex, 1); - - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); - - return ResponseHandler.ok("Host removed successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - scheduler(req: Request) { - const ResponseHandler = createResponseHandler(this.res); - try { - const interval = req.query.interval as string; - const newInterval = parseInterval(interval); - - if (newInterval < 5 * 60 * 1000 || newInterval > 6 * 60 * 60 * 1000) { - return ResponseHandler.error( - "Interval must be between 5 minutes and 6 hours.", - 401, - ); - } - - setFetchInterval(newInterval); - return ResponseHandler.ok("Updated interval"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } -} - -export const createConfHandler = (req: Request, res: Response) => - new ConfHandler(req, res); diff --git a/src/handlers/data.ts b/src/handlers/data.ts deleted file mode 100644 index 5d3bf41c..00000000 --- a/src/handlers/data.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Response, Request } from "express"; -import db from "../config/db"; -import { Table, DataRow } from "../typings/table"; -import { createResponseHandler } from "./response"; -import logger from "../utils/logger"; - -function formatRows(rows: DataRow[]): Record { - return rows.reduce( - ( - acc: Record, - row, - index: number, - ): Record => { - acc[index] = JSON.parse(row.info); - return acc; - }, - {}, - ); -} - -class DatabaseHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - latest() { - const ResponseHandler = createResponseHandler(this.res); - db.get( - "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", - (error: unknown, row: Partial> | undefined) => { - if (error) { - return ResponseHandler.critical(error as string); - } - - if (!row || !row.info) { - return ResponseHandler.error( - "No data available for /data/latest", - 404, - ); - } - - try { - return ResponseHandler.rawData( - JSON.parse(row.info), - "Read latest data", - ); - } catch (error: unknown) { - const errorMsg = - error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - }, - ); - } - - latestRaw(): Promise { - return new Promise((resolve, reject) => { - logger.debug("Reading DB"); - db.get( - "SELECT info FROM data ORDER BY timestamp DESC LIMIT 1", - (error: unknown, row: Partial> | undefined) => { - if (error) { - return reject(`Database query error: ${error}`); - } - - if (!row || !row.info) { - return reject("No data available for /data/latest"); - } - - try { - logger.info("Read latest data"); - const parsedData = JSON.parse(row.info); - logger.debug("Parsed data:", parsedData); - resolve(parsedData); - } catch (error: unknown) { - const errorMsg = - error instanceof Error ? error.message : String(error); - reject(`Error parsing data: ${errorMsg}`); - } - }, - ); - }); - } - - all() { - const ResponseHandler = createResponseHandler(this.res); - const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); - - db.all( - "SELECT info FROM data WHERE timestamp >= ?", - [oneDayAgo], - (error: unknown, rows: Pick[] | undefined) => { - if (error) { - return ResponseHandler.critical(error as string); - } - - if (!rows || rows.length === 0) { - return ResponseHandler.error("No data available", 404); - } - - return ResponseHandler.rawData(formatRows(rows), "Read database"); - }, - ); - } - - clear() { - const ResponseHandler = createResponseHandler(this.res); - db.run("DELETE FROM data", (error: unknown) => { - if (error) { - return ResponseHandler.critical(error as string); - } - - return ResponseHandler.ok("Database cleared successfully"); - }); - } -} - -export const createDatabaseHandler = (req: Request, res: Response) => - new DatabaseHandler(req, res); diff --git a/src/handlers/frontend.ts b/src/handlers/frontend.ts deleted file mode 100644 index 6b2edc55..00000000 --- a/src/handlers/frontend.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Request, Response } from "express"; -import { createResponseHandler } from "./response"; -import { - hideContainer, - unhideContainer, - addTagToContainer, - removeTagFromContainer, - pinContainer, - unpinContainer, - setLink, - removeLink, - setIcon, - removeIcon, -} from "../controllers/frontendConfiguration"; - -class FrontendHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - async show(containerName: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await unhideContainer(containerName); - return ResponseHandler.ok("Container unhidden successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async hide(containerName: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await hideContainer(containerName); - return ResponseHandler.ok("Hid container succesfully"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async addTag(containerName: string, tag: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await addTagToContainer(containerName, tag); - return ResponseHandler.ok("Tag added successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async removeTag(containerName: string, tag: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await removeTagFromContainer(containerName, tag); - ResponseHandler.ok("Tag removed successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async pin(containerName: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await pinContainer(containerName); - return ResponseHandler.ok("Container pinned successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async unPin(containerName: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await unpinContainer(containerName); - return ResponseHandler.ok("Container unpinned successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async addLink(containerName: string, link: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await setLink(containerName, link); - return ResponseHandler.ok("Link added successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async removeLink(containerName: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await removeLink(containerName); - return ResponseHandler.ok("Removed link succesfully"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async addIcon(containerName: string, icon: string, useCustomIcon: string) { - const ResponseHandler = createResponseHandler(this.res); - const iconBool = useCustomIcon === "true"; - try { - await setIcon(containerName, icon, iconBool); - return ResponseHandler.ok("Icon added successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async removeIcon(containerName: string) { - const ResponseHandler = createResponseHandler(this.res); - try { - await removeIcon(containerName); - return ResponseHandler.ok("Icon removed successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } -} - -export const createFrontendHandler = (req: Request, res: Response) => - new FrontendHandler(req, res); diff --git a/src/handlers/graph.ts b/src/handlers/graph.ts deleted file mode 100644 index 12e05724..00000000 --- a/src/handlers/graph.ts +++ /dev/null @@ -1,82 +0,0 @@ -import cytoscape from "cytoscape"; -import logger from "../utils/logger"; -import { AllContainerData, ContainerData } from "./../typings/dockerConfig"; -import { atomicWrite } from "../utils/atomicWrite"; - -const CACHE_DIR_JSON = "./src/data/graph.json"; - -async function generateGraphJSON( - allContainerData: AllContainerData, -): Promise { - try { - logger.info("generateGraphJSON >>> Starting generation"); - - const graphData = { - nodes: [] as cytoscape.ElementDefinition[], - edges: [] as cytoscape.ElementDefinition[], - }; - - for (const [hostName, containers] of Object.entries(allContainerData)) { - if ("error" in containers) { - graphData.nodes.push({ - data: { - id: hostName, - label: `Host: ${hostName} Error: ${containers.error}`, - type: "server", - error: true, - }, - }); - } else { - const containerList = containers as ContainerData[]; - - // Host node with container count and metadata - graphData.nodes.push({ - data: { - id: hostName, - label: `${hostName}\n${containerList.length} Containers`, - type: "server", - hostName, - containerCount: containerList.length, - }, - }); - - for (const container of containerList) { - const { id, ...otherContainerProps } = container; - - graphData.nodes.push({ - data: { - id: id, - label: `${container.name}\n${container.state.toUpperCase()}`, - type: "container", - ...otherContainerProps, - }, - }); - - // Edge between host and container - graphData.edges.push({ - data: { - id: `${hostName}-${container.id}`, - source: hostName, - target: container.id, - connectionType: "host-container", - }, - }); - } - } - } - - atomicWrite(CACHE_DIR_JSON, JSON.stringify(graphData, null, 2)); - logger.info("generateGraphJSON <<< JSON file generated successfully"); - return true; - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return false; - } -} - -function getGraphFilePath() { - return { json: CACHE_DIR_JSON }; -} - -export { generateGraphJSON, getGraphFilePath }; diff --git a/src/handlers/ha.ts b/src/handlers/ha.ts deleted file mode 100644 index 16c9ae19..00000000 --- a/src/handlers/ha.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Request, Response } from "express"; -import logger from "../utils/logger"; -import { - readConfig, - prepareFilesForSync, - ensureFileExists, -} from "../controllers/highAvailability"; -import { createResponseHandler } from "./response"; - -class HaHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - async config() { - const ResponseHandler = createResponseHandler(this.res); - try { - const data = await readConfig(); - return ResponseHandler.rawData(data, "Fetched HA-Config"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async sync(req: Request) { - const ResponseHandler = createResponseHandler(this.res); - try { - const { files } = req.body; - logger.info("Received synchronization request from master node."); - if (!files || typeof files !== "object") { - return ResponseHandler.error( - "Invalid request: 'files' object is missing or invalid.", - 400, - ); - } - - for (const [filePath, content] of Object.entries(files)) { - await ensureFileExists(filePath, content as string); - } - - return ResponseHandler.ok("Synchronization completed successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async prepare() { - const ResponseHandler = createResponseHandler(this.res); - try { - logger.info("Preparing files for synchronization."); - const fileData = await prepareFilesForSync(); - return ResponseHandler.rawData( - fileData, - "Done preparing files for synchronization", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } -} - -export const createHaHandler = (req: Request, res: Response) => - new HaHandler(req, res); diff --git a/src/handlers/notification.ts b/src/handlers/notification.ts deleted file mode 100644 index 9c10a599..00000000 --- a/src/handlers/notification.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Request, Response } from "express"; -import fs from "fs"; -import notify from "../utils/notifications/_notify"; -const dataTemplate = "./src/data/template.json"; -import { TemplateData } from "../typings/template"; -import { createResponseHandler } from "./response"; - -function isTemplateData(data: TemplateData): data is TemplateData { - return ( - data !== null && typeof data === "object" && typeof data.text === "string" - ); -} - -class NotificationHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - getTemplate() { - const ResponseHandler = createResponseHandler(this.res); - try { - fs.readFile(dataTemplate, "utf-8", (error: unknown, data) => { - if (error) { - return ResponseHandler.error(error as string, 400); - } - return ResponseHandler.rawData( - JSON.parse(data), - "Fetched notification template", - ); - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - setTemplate(req: Request) { - const ResponseHandler = createResponseHandler(this.res); - const newTemplate: TemplateData = req.body; - - try { - if (!isTemplateData(newTemplate)) { - return ResponseHandler.error( - "Invalid input format. Expected JSON with a 'text' field.", - 400, - ); - } - - fs.writeFileSync(dataTemplate, JSON.stringify(newTemplate, null, 2)); - return ResponseHandler.ok("Template updated successfully."); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async test(req: Request) { - const { type, containerId } = req.params; - const ResponseHandler = createResponseHandler(this.res); - - try { - await notify(type, containerId); - return ResponseHandler.ok("Sent test notification"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } -} - -export const createNotificationHandler = (req: Request, res: Response) => - new NotificationHandler(req, res); diff --git a/src/handlers/response.ts b/src/handlers/response.ts deleted file mode 100644 index ee062102..00000000 --- a/src/handlers/response.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Response } from "express"; -import logger from "../utils/logger"; - -class ResponseHandler { - private res: Response; - - constructor(res: Response) { - this.res = res; - } - - rawData(data: unknown, message: string) { - logger.info(message); - this.res.status(200).json(data); - } - - ok(message: string) { - logger.info(message); - this.res.status(200).json({ status: "success", message }); - } - - denied(message: string) { - logger.warn(message); - this.res.status(403).json({ status: "denied", message }); - } - - error(message: string, code: number) { - logger.error(`Code: ${code} - ${message}`); - this.res.status(code).json({ status: "error", message }); - } - - critical(log: string) { - logger.error(log.replace(/\n|\r/g, "")); - this.res.status(500).json({ - status: "critical", - message: "Please see the server logs for more info", - }); - } -} - -export const createResponseHandler = (res: Response) => - new ResponseHandler(res); diff --git a/src/handlers/stack.ts b/src/handlers/stack.ts deleted file mode 100644 index 0f15e166..00000000 --- a/src/handlers/stack.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { Response, Request } from "express"; -import { - createStack, - getStackConfig, - getStackCompose, - writeEnvFile, - getEnvFile, -} from "../config/stacks"; -import { DockerComposeFile } from "../typings/dockerCompose"; -import logger from "../utils/logger"; -import * as compose from "docker-compose"; -import { createResponseHandler } from "./response"; -import { stackConfig } from "../typings/stackConfig"; -import { dockerStackEnv } from "../typings/dockerStackEnv"; -import path from "path"; - -const PROJECT_ROOT = path.resolve(__dirname, "../.."); - -export async function validate(name: string): Promise { - const config: stackConfig = JSON.parse(await getStackConfig()); - if (!config.stacks.find((element) => element === name)) { - throw new Error("Stack not found"); - } - - return true; -} - -async function composeAction(option: string, name: string): Promise { - const composeFile: string = path.join(PROJECT_ROOT, `stacks/${name}`); - try { - switch (option) { - case "start": { - await compose.upAll({ cwd: composeFile, log: false }); - break; - } - case "stop": { - await compose.downAll({ cwd: composeFile, log: false }); - break; - } - default: - throw new Error(`Invalid option: ${option}`); - } - } catch (err) { - let errorMessage: string; - const portAllocated: string = "port is already allocated"; - - if (err instanceof Error) { - errorMessage = err.message; - } else if (typeof err === "object" && err !== null) { - errorMessage = JSON.stringify(err); - } else { - errorMessage = String(err); - } - - if (errorMessage.search(portAllocated)) { - logger.error("Port(s) already allocated"); - } - throw new Error(errorMessage); - } -} - -class StackHandler { - private req: Request; - private res: Response; - - constructor(req: Request, res: Response) { - this.req = req; - this.res = res; - } - - async createStack(req: Request, res: Response) { - const ResponseHandler = createResponseHandler(res); - try { - const name: string = req.params.name; - const content: DockerComposeFile = req.body; - let override = false; - override = req.query.override == "true"; - - await createStack(name, content, override); - return ResponseHandler.ok("Stack created"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async start(req: Request, res: Response) { - const ResponseHandler = createResponseHandler(res); - try { - const name: string = req.params.name; - await validate(name); - await composeAction("start", name); - return ResponseHandler.ok("Stack started"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async stop(req: Request, res: Response) { - const ResponseHandler = createResponseHandler(res); - try { - const name: string = req.params.name; - await validate(name); - await composeAction("stop", name); - return ResponseHandler.ok("Stack stopped"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } - } - - async stackCompose(req: Request, res: Response) { - const ResponseHandler = createResponseHandler(res); - try { - const { name } = req.params; - return ResponseHandler.rawData( - await getStackCompose(name), - "Stack compose fetched", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg.replace(/\n|\r/g, "")); - throw new Error(errorMsg); - } - } - - async setStackEnv(req: Request, res: Response) { - const ResponseHandler = createResponseHandler(res); - try { - const data: dockerStackEnv = req.body; - const name: string = req.params.name; - if (await writeEnvFile(name, data)) { - return ResponseHandler.ok("Wrote docker.env"); - } else { - return ResponseHandler.critical( - "Something went wrong while writing the env File!", - ); - } - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg.replace(/\n|\r/g, "")); - throw new Error(errorMsg); - } - } - - async getStackEnv(req: Request, res: Response) { - const ResponseHandler = createResponseHandler(res); - try { - const name: string = req.params.name; - const data = await getEnvFile(name); - if (data == null) { - return ResponseHandler.error( - "No environment file found for this Stack!", - 404, - ); - } - return ResponseHandler.rawData(data, "Read docker.env"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg.replace(/\n|\r/g, "")); - throw new Error(errorMsg); - } - } -} - -export const createStackHandler = (req: Request, res: Response) => - new StackHandler(req, res); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..dcca5a9a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,45 @@ +import { Elysia } from "elysia"; +import { swagger } from "@elysiajs/swagger"; +import { loadPlugins } from "~/core/plugins/loader"; +import { dockerRoutes } from "~/routes/docker"; +import { logRoutes } from "~/routes/container-logs"; +import { backendLogs } from "./routes/logs"; +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; + +dbFunctions.init(); + +const app = new Elysia() + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "0.1.0", + description: "Docker monitoring API with plugin support", + }, + }, + }), + ) + .use(dockerRoutes) + .use(logRoutes) + .use(backendLogs) + .get("/health", () => ({ status: "healthy" })); + +async function startServer() { + try { + await loadPlugins("./plugins"); + + app.listen(3000, ({ hostname, port }) => { + logger.info(`🦊 Elysia is running at http://${hostname}:${port}`); + logger.info( + `📚 API Documentation available at http://${hostname}:${port}/swagger`, + ); + }); + } catch (error) { + logger.error("Failed to start server:", error); + process.exit(1); + } +} + +startServer(); diff --git a/src/init.ts b/src/init.ts deleted file mode 100644 index 188542f6..00000000 --- a/src/init.ts +++ /dev/null @@ -1,69 +0,0 @@ -import express, { Request, Response, NextFunction } from "express"; -import process from "node:process"; -import swaggerDocs from "./utils/swaggerDocs"; -import auth from "./routes/auth/routes"; -import data from "./routes/data/routes"; -import frontend from "./routes/frontendController/routes"; -import api from "./routes/getter/routes"; -import notificationService from "./routes/notifications/routes"; -import conf from "./routes/setter/routes"; -import graph from "./routes/graphs/routes"; -import authMiddleware from "./middleware/authMiddleware"; -import ha from "./routes/highavailability/routes"; -import trustedProxies from "./controllers/proxy"; -import { limiter } from "./middleware/rateLimiter"; -import { scheduleFetch } from "./controllers/scheduler"; -import { Server } from 'http'; -import cors from "cors"; -import { setupWebSocket } from "./utils/webSocket"; -import stacks from "./routes/stack/routes"; -import { blockWhileLocked } from "./middleware/checkLock"; -import logger from "./utils/logger"; -import initFiles from "./config/initFiles"; - -const LAB = [limiter, authMiddleware, blockWhileLocked]; - -const initializeApp = (app: express.Application, server: Server): void => { - initFiles(); - - try { - logger.debug("Starting Websocket server, with these endpoints:"); - logger.debug("ws://localhost:9876/wss/container-data") - logger.debug("ws://localhost:9876/wss/server-logs") - setupWebSocket(server); - } catch (error: unknown) { - logger.error("Error starting WebSocket: ", error) - } - - app.use(cors()); - app.use(express.json()); - - if (process.env.NODE_ENV !== "production") { - app.use("/api-docs", (req: Request, res: Response, next: NextFunction) => - next(), - ); - app.get("/", (req: Request, res: Response) => { - res.redirect("/api-docs"); - }); - swaggerDocs(app); - } - - trustedProxies(app); - scheduleFetch(); - - app.use("/api", LAB, api); - app.use("/conf", LAB, conf); - app.use("/auth", LAB, auth); - app.use("/data", LAB, data); - app.use("/frontend", LAB, frontend); - app.use("/graph", LAB, graph); - app.use("/notification-service", LAB, notificationService); - app.use("/stacks", LAB, stacks); - app.use("/ha", limiter, authMiddleware, ha); - - process.on("exit", (code: number) => { - logger.warn(`Server exiting (Code: ${code})`); - }); -}; - -export default initializeApp; diff --git a/src/middleware/authMiddleware.ts b/src/middleware/authMiddleware.ts deleted file mode 100644 index 414b2762..00000000 --- a/src/middleware/authMiddleware.ts +++ /dev/null @@ -1,51 +0,0 @@ -import bcrypt from "bcrypt"; -import { Request, Response, NextFunction } from "express"; -import logger from "../utils/logger"; -import { rateLimitedReadFile } from "../utils/rateLimitFS"; -import { createResponseHandler } from "../handlers/response"; -const passwordFile = "./src/data/password.json"; -const passwordBool = "./src/data/usePassword.txt"; - -async function authMiddleware( - req: Request, - res: Response, - next: NextFunction, -): Promise { - const ResponseHandler = createResponseHandler(res); - try { - const authStatusData = await rateLimitedReadFile(passwordBool); - const isAuthEnabled = authStatusData.trim() === "true"; - - if (!isAuthEnabled) { - logger.warn("You are not using authentication, please enable it."); - logger.debug("Authentication disabled, skipping login process..."); - return next(); - } - - const providedPassword = req.headers["x-password"]; - if (!providedPassword) { - ResponseHandler.denied("Password required"); - return; - } - - const passwordData = await rateLimitedReadFile(passwordFile); - const storedData = JSON.parse(passwordData); - - const passwordMatch = await bcrypt.compare( - providedPassword as string, - storedData.hash, - ); - if (!passwordMatch) { - ResponseHandler.error("Invalid Password", 402); - return; - } - - logger.debug("Authentication succesfull"); - next(); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } -} - -export default authMiddleware; diff --git a/src/middleware/checkLock.ts b/src/middleware/checkLock.ts deleted file mode 100644 index c01540fe..00000000 --- a/src/middleware/checkLock.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { rateLimitedExistsSync } from "../utils/rateLimitFS"; -import { createResponseHandler } from "../handlers/response"; - -const lockFilePath = "./src/data/ha.lock"; - -export async function blockWhileLocked( - req: Request, - res: Response, - next: NextFunction, -): Promise { - const ResponseHandler = createResponseHandler(res); - if (await rateLimitedExistsSync(lockFilePath)) { - ResponseHandler.error( - "Service unavailable. The high-availability lock is currently active. Please try again later.", - 503, - ); - } else { - next(); - } -} diff --git a/src/middleware/rateLimiter.ts b/src/middleware/rateLimiter.ts deleted file mode 100644 index dc64af25..00000000 --- a/src/middleware/rateLimiter.ts +++ /dev/null @@ -1,8 +0,0 @@ -import rateLimit from "express-rate-limit"; - -export const limiter = rateLimit({ - windowMs: 5 * 60 * 1000, // 5 minutes - limit: 300, // Limit each IP to 300 requests per `window` (here, per 5 minutes) - standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers - legacyHeaders: false, // Disable the `X-RateLimit-*` headers -}); diff --git a/src/misc/.tmux.sh b/src/misc/.tmux.sh deleted file mode 100644 index a929a1a3..00000000 --- a/src/misc/.tmux.sh +++ /dev/null @@ -1 +0,0 @@ -[ -z "$TMUX" ] && tmux new-session -d -s docker 'docker compose -f docker/docker-compose.yaml logs -f master' \; rename-window 'master' \; new-window 'docker compose -f docker/docker-compose.yaml logs -f slave' \; rename-window 'slave' \; new-window 'docker compose -f docker/docker-compose.yaml logs -f test-socket-proxy' \; rename-window 'proxy' \; attach-session || echo 'Already inside a tmux session. Exiting.' diff --git a/src/misc/createEnvDev.sh b/src/misc/createEnvDev.sh deleted file mode 100755 index 1f231aa6..00000000 --- a/src/misc/createEnvDev.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -# Version -VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" - -# Automatic Stack environment management -AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT="${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT:-true}" - -# Docker -if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then - RUNNING_IN_DOCKER="true" -else - RUNNING_IN_DOCKER="false" -fi - -# Default dev log level -LOG_LEVEL="${LOG_LEVEL:-debug}" - -echo -n "\ -{ - \"VERSION\": \"${VERSION}\", - \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", - \"TRUSTED_PROXIES\": \"${TRUSTED_PROXIES}\", - \"HA_MASTER\": \"${HA_MASTER}\", - \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", - \"HA_NODE\": \"${HA_NODE}\", - \"HA_UNSAFE\": \"${HA_UNSAFE}\", - \"DISCORD_WEBHOOK_URL\": \"${DISCORD_WEBHOOK_URL}\", - \"EMAIL_SENDER\": \"${EMAIL_SENDER}\", - \"EMAIL_RECIPIENT\": \"${EMAIL_RECIPIENT}\", - \"EMAIL_PASSWORD\": \"${EMAIL_PASSWORD}\", - \"EMAIL_SERVICE\": \"${EMAIL_SERVICE}\", - \"PUSHBULLET_ACCESS_TOKEN\": \"${PUSHBULLET_ACCESS_TOKEN}\", - \"PUSHOVER_USER_KEY\": \"${PUSHOVER_USER_KEY}\", - \"PUSHOVER_API_TOKEN\": \"${PUSHOVER_API_TOKEN}\", - \"SLACK_WEBHOOK_URL\": \"${SLACK_WEBHOOK_URL}\", - \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", - \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", - \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", - \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\", - \"AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT\": \"${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT}\", - \"LOG_LEVEL\": \"${LOG_LEVEL}\" -} \ -" > ./src/data/variables.json || exit 1 diff --git a/src/misc/createEnvFile.sh b/src/misc/createEnvFile.sh deleted file mode 100755 index 0fbd15de..00000000 --- a/src/misc/createEnvFile.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -# Version -VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" - -# Automatic Stack environment management -AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT="${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT:-true}" - -# Docker -if grep -q '/docker' /proc/1/cgroup 2>/dev/null || [ -f /.dockerenv ]; then - RUNNING_IN_DOCKER="true" -else - RUNNING_IN_DOCKER="false" -fi - -# Default log level -LOG_LEVEL="${LOG_LEVEL:-info}" - -echo -n "\ -{ - \"VERSION\": \"${VERSION}\", - \"RUNNING_IN_DOCKER\": \"${RUNNING_IN_DOCKER}\", - \"TRUSTED_PROXIES\": \"${TRUSTED_PROXIES}\", - \"HA_MASTER\": \"${HA_MASTER}\", - \"HA_MASTER_IP\": \"${HA_MASTER_IP}\", - \"HA_NODE\": \"${HA_NODE}\", - \"HA_UNSAFE\": \"${HA_UNSAFE}\", - \"DISCORD_WEBHOOK_URL\": \"${DISCORD_WEBHOOK_URL}\", - \"EMAIL_SENDER\": \"${EMAIL_SENDER}\", - \"EMAIL_RECIPIENT\": \"${EMAIL_RECIPIENT}\", - \"EMAIL_PASSWORD\": \"${EMAIL_PASSWORD}\", - \"EMAIL_SERVICE\": \"${EMAIL_SERVICE}\", - \"PUSHBULLET_ACCESS_TOKEN\": \"${PUSHBULLET_ACCESS_TOKEN}\", - \"PUSHOVER_USER_KEY\": \"${PUSHOVER_USER_KEY}\", - \"PUSHOVER_API_TOKEN\": \"${PUSHOVER_API_TOKEN}\", - \"SLACK_WEBHOOK_URL\": \"${SLACK_WEBHOOK_URL}\", - \"TELEGRAM_BOT_TOKEN\": \"${TELEGRAM_BOT_TOKEN}\", - \"TELEGRAM_CHAT_ID\": \"${TELEGRAM_CHAT_ID}\", - \"WHATSAPP_API_URL\": \"${WHATSAPP_API_URL}\", - \"WHATSAPP_RECIPIENT\": \"${WHATSAPP_RECIPIENT}\", - \"AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT\": \"${AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT}\", - \"LOG_LEVEL\": \"${LOG_LEVEL}\" -} \ -" > /api/src/data/variables.json || exit 1 diff --git a/src/misc/credits.sh b/src/misc/credits.sh deleted file mode 100755 index 3db14f64..00000000 --- a/src/misc/credits.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -if ! command -v jq 2>&1 >/dev/null -then - echo "ERROR: jq could not be found" - exit 1 -fi - - -LICENSE_JSON=$(npx license-checker \ - --exclude 'MIT, MIT-0, MIT OR X11, BSD, ISC, Unlicense, CC0-1.0, Python-2.0: 1' \ - --json) - -{ - echo -e "# CREDITS\n" - echo -e "This file shows all npm packages used in DockStatAPI (also Dev packages)\n" -} > CREDITS.md - -jq -r ' - to_entries | - group_by(.value.licenses)[] | - "### License: \(.[0].value.licenses)\n\n" + - "| Name | Repository | Publisher |\n|------|-------------|-----------|\n" + - (map( - "| \(.key) | \(.value.repository // "N/A") | \(.value.publisher // "N/A") |" - ) | join("\n")) + "\n\n" -' <<< "$LICENSE_JSON" >> CREDITS.md - -echo "Markdown file with license information has been created: CREDITS.md" diff --git a/src/misc/dependencyGraphs/.dependency-cruiser.cjs b/src/misc/dependencyGraphs/.dependency-cruiser.cjs deleted file mode 100644 index d734a682..00000000 --- a/src/misc/dependencyGraphs/.dependency-cruiser.cjs +++ /dev/null @@ -1,359 +0,0 @@ -/** @type {import('dependency-cruiser').IConfiguration} */ -module.exports = { - forbidden: [ - { - name: "no-circular", - severity: "warn", - comment: - "This dependency is part of a circular relationship. You might want to revise " + - "your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ", - from: {}, - to: { - circular: true, - }, - }, - { - name: "no-orphans", - comment: - "This is an orphan module - it's likely not used (anymore?). Either use it or " + - "remove it. If it's logical this module is an orphan (i.e. it's a config file), " + - "add an exception for it in your dependency-cruiser configuration. By default " + - "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + - "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", - severity: "warn", - from: { - orphan: true, - pathNot: [ - "(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$", // dot files - "[.]d[.]ts$", // TypeScript declaration files - "(^|/)tsconfig[.]json$", // TypeScript config - "(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$", // other configs - ], - }, - to: {}, - }, - { - name: "no-deprecated-core", - comment: - "A module depends on a node core module that has been deprecated. Find an alternative - these are " + - "bound to exist - node doesn't deprecate lightly.", - severity: "warn", - from: {}, - to: { - dependencyTypes: ["core"], - path: [ - "^v8/tools/codemap$", - "^v8/tools/consarray$", - "^v8/tools/csvparser$", - "^v8/tools/logreader$", - "^v8/tools/profile_view$", - "^v8/tools/profile$", - "^v8/tools/SourceMap$", - "^v8/tools/splaytree$", - "^v8/tools/tickprocessor-driver$", - "^v8/tools/tickprocessor$", - "^node-inspect/lib/_inspect$", - "^node-inspect/lib/internal/inspect_client$", - "^node-inspect/lib/internal/inspect_repl$", - "^async_hooks$", - "^punycode$", - "^domain$", - "^constants$", - "^sys$", - "^_linklist$", - "^_stream_wrap$", - ], - }, - }, - { - name: "not-to-deprecated", - comment: - "This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later " + - "version of that module, or find an alternative. Deprecated modules are a security risk.", - severity: "warn", - from: {}, - to: { - dependencyTypes: ["deprecated"], - }, - }, - { - name: "no-non-package-json", - severity: "error", - comment: - "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + - "That's problematic as the package either (1) won't be available on live (2 - worse) will be " + - "available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " + - "in your package.json.", - from: {}, - to: { - dependencyTypes: ["npm-no-pkg", "npm-unknown"], - }, - }, - { - name: "not-to-unresolvable", - comment: - "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + - "module: add it to your package.json. In all other cases you likely already know what to do.", - severity: "error", - from: {}, - to: { - couldNotResolve: true, - }, - }, - { - name: "no-duplicate-dep-types", - comment: - "Likely this module depends on an external ('npm') package that occurs more than once " + - "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + - "maintenance problems later on.", - severity: "warn", - from: {}, - to: { - moreThanOneDependencyType: true, - // as it's pretty common to have a type import be a type only import - // _and_ (e.g.) a devDependency - don't consider type-only dependency - // types for this rule - dependencyTypesNot: ["type-only"], - }, - }, - - /* rules you might want to tweak for your specific situation: */ - - { - name: "not-to-spec", - comment: - "This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. " + - "If there's something in a spec that's of use to other modules, it doesn't have that single " + - "responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.", - severity: "error", - from: {}, - to: { - path: "[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$", - }, - }, - { - name: "not-to-dev-dep", - severity: "error", - comment: - "This module depends on an npm package from the 'devDependencies' section of your " + - "package.json. It looks like something that ships to production, though. To prevent problems " + - "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + - "section of your package.json. If this module is development only - add it to the " + - "from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration", - from: { - path: "^(./)", - pathNot: "[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$", - }, - to: { - dependencyTypes: ["npm-dev"], - // type only dependencies are not a problem as they don't end up in the - // production code or are ignored by the runtime. - dependencyTypesNot: ["type-only"], - pathNot: ["node_modules/@types/"], - }, - }, - { - name: "optional-deps-used", - severity: "info", - comment: - "This module depends on an npm package that is declared as an optional dependency " + - "in your package.json. As this makes sense in limited situations only, it's flagged here. " + - "If you're using an optional dependency here by design - add an exception to your" + - "dependency-cruiser configuration.", - from: {}, - to: { - dependencyTypes: ["npm-optional"], - }, - }, - { - name: "peer-deps-used", - comment: - "This module depends on an npm package that is declared as a peer dependency " + - "in your package.json. This makes sense if your package is e.g. a plugin, but in " + - "other cases - maybe not so much. If the use of a peer dependency is intentional " + - "add an exception to your dependency-cruiser configuration.", - severity: "warn", - from: {}, - to: { - dependencyTypes: ["npm-peer"], - }, - }, - ], - options: { - /* Which modules not to follow further when encountered */ - doNotFollow: { - /* path: an array of regular expressions in strings to match against */ - path: ["../node_modules"], - }, - - /* Which modules to exclude */ - // exclude : { - // /* path: an array of regular expressions in strings to match against */ - // path: '', - // }, - - /* Which modules to exclusively include (array of regular expressions in strings) - dependency-cruiser will skip everything not matching this pattern - */ - // includeOnly : [''], - - /* List of module systems to cruise. - When left out dependency-cruiser will fall back to the list of _all_ - module systems it knows of. It's the default because it's the safe option - It might come at a performance penalty, though. - moduleSystems: ['amd', 'cjs', 'es6', 'tsd'] - - As in practice only commonjs ('cjs') and ecmascript modules ('es6') - are widely used, you can limit the moduleSystems to those. - */ - - // moduleSystems: ['cjs', 'es6'], - - /* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/main/' - to open it on your online repo or `vscode://file/${process.cwd()}/` to - open it in visual studio code), - */ - // prefix: `vscode://file/${process.cwd()}/`, - - /* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation - true: also detect dependencies that only exist before typescript-to-javascript compilation - "specify": for each dependency identify whether it only exists before compilation or also after - */ - // tsPreCompilationDeps: false, - - /* list of extensions to scan that aren't javascript or compile-to-javascript. - Empty by default. Only put extensions in here that you want to take into - account that are _not_ parsable. - */ - // extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"], - - /* if true combines the package.jsons found from the module up to the base - folder the cruise is initiated from. Useful for how (some) mono-repos - manage dependencies & dependency definitions. - */ - // combinedDependencies: false, - - /* if true leave symlinks untouched, otherwise use the realpath */ - // preserveSymlinks: false, - - /* TypeScript project file ('tsconfig.json') to use for - (1) compilation and - (2) resolution (e.g. with the paths property) - - The (optional) fileName attribute specifies which file to take (relative to - dependency-cruiser's current working directory). When not provided - defaults to './tsconfig.json'. - */ - //tsConfig: { - //fileName: "../tsconfig.json", - //}, - - /* Webpack configuration to use to get resolve options from. - - The (optional) fileName attribute specifies which file to take (relative - to dependency-cruiser's current working directory. When not provided defaults - to './webpack.conf.js'. - - The (optional) `env` and `arguments` attributes contain the parameters - to be passed if your webpack config is a function and takes them (see - webpack documentation for details) - */ - // webpackConfig: { - // fileName: 'webpack.config.js', - // env: {}, - // arguments: {} - // }, - - /* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use - for compilation - */ - // babelConfig: { - // fileName: '.babelrc', - // }, - - /* List of strings you have in use in addition to cjs/ es6 requires - & imports to declare module dependencies. Use this e.g. if you've - re-declared require, use a require-wrapper or use window.require as - a hack. - */ - // exoticRequireStrings: [], - - /* options to pass on to enhanced-resolve, the package dependency-cruiser - uses to resolve module references to disk. The values below should be - suitable for most situations - - If you use webpack: you can also set these in webpack.conf.js. The set - there will override the ones specified here. - */ - enhancedResolveOptions: { - /* What to consider as an 'exports' field in package.jsons */ - exportsFields: ["exports"], - /* List of conditions to check for in the exports field. - Only works when the 'exportsFields' array is non-empty. - */ - conditionNames: ["import", "require", "node", "default", "types"], - /* - The extensions, by default are the same as the ones dependency-cruiser - can access (run `npx depcruise --info` to see which ones that are in - _your_ environment). If that list is larger than you need you can pass - the extensions you actually use (e.g. ["", ".jsx"]). This can speed - up module resolution, which is the most expensive step. - */ - extensions: ["", ".jsx", ".ts", ".tsx"], - /* What to consider a 'main' field in package.json */ - mainFields: ["module", "main", "types", "typings"], - /* - A list of alias fields in package.jsons - See [this specification](https://github.com/defunctzombie/package-browser-field-spec) and - the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields) - documentation - - Defaults to an empty array (= don't use alias fields). - */ - // aliasFields: ["browser"], - }, - reporterOptions: { - dot: { - /* pattern of modules that can be consolidated in the detailed - graphical dependency graph. The default pattern in this configuration - collapses everything in node_modules to one folder deep so you see - the external modules, but their innards. - */ - collapsePattern: "node_modules/(?:@[^/]+/[^/]+|[^/]+)", - - /* Options to tweak the appearance of your graph.See - https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions - for details and some examples. If you don't specify a theme - dependency-cruiser falls back to a built-in one. - */ - theme: { - graph: { - /* splines: "ortho" gives straight lines, but is slow on big graphs - splines: "true" gives bezier curves (fast, not as nice as ortho) - */ - ortho: "true", - }, - }, - }, - archi: { - /* pattern of modules that can be consolidated in the high level - graphical dependency graph. If you use the high level graphical - dependency graph reporter (`archi`) you probably want to tweak - this collapsePattern to your situation. - */ - collapsePattern: - "^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)", - - /* Options to tweak the appearance of your graph. If you don't specify a - theme for 'archi' dependency-cruiser will use the one specified in the - dot section above and otherwise use the default one. - */ - // theme: { }, - }, - text: { - highlightFocused: true, - }, - }, - }, -}; -// generated: dependency-cruiser@16.5.0 on 2024-11-08T20:57:37.261Z diff --git a/src/misc/dependencyGraphs/createDependencyGraph.sh b/src/misc/dependencyGraphs/createDependencyGraph.sh deleted file mode 100755 index 5fe007aa..00000000 --- a/src/misc/dependencyGraphs/createDependencyGraph.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash -TMP=$(mktemp) -IGNORE="node_modules|logger|.dependency-cruiser|path|fs|os|https|net|process|util" - -cat ./src/init.ts | grep "./routes" | awk '{print $2,$4}' > $TMP - -spawn_worker(){ - local line="$1" - local target_route="$(echo "$line" | cut -d '"' -f2 | sed 's|^./routes|./src/routes|').ts" - local route=$(echo "$line" | awk '{print $1}') - - echo -e "\nRoute: $route \n${target_route}" - - test=true depcruise \ - -c ./src/misc/dependencyGraphs/.dependency-cruiser.cjs \ - -p cli-feedback \ - -T mermaid \ - -x "$IGNORE" \ - -f ./src/misc/dependencyGraphs/mermaid-${route}.txt \ - ${target_route} || exit 1 -} - -while read line; do - spawn_worker "$line" & -done < <(cat $TMP) - -npx depcruise \ - -c ./src/misc/dependencyGraphs/.dependency-cruiser.cjs \ - -p cli-feedback \ - -T mermaid \ - -x "$IGNORE" \ - -f ./src/misc/dependencyGraphs/mermaid-all.txt \ - ./src/server.ts || exit 1 - -wait - -find ./src/misc/dependencyGraphs -type f -name "*.txt" -exec sed -i 's/flowchart LR/flowchart TB/g' {} + - -echo -e "\n========\n\n DONE\n\n========" - -exit 0 diff --git a/src/misc/dependencyGraphs/mermaid-all.txt b/src/misc/dependencyGraphs/mermaid-all.txt deleted file mode 100644 index 1cb2ebe8..00000000 --- a/src/misc/dependencyGraphs/mermaid-all.txt +++ /dev/null @@ -1,113 +0,0 @@ -flowchart TB - -subgraph 0["src"] -1["server.ts"] -2["init.ts"] -subgraph 3["config"] -4["initFiles.ts"] -7["variables.ts"] -B["db.ts"] -end -subgraph 5["controllers"] -6["proxy.ts"] -A["scheduler.ts"] -C["fetchData.ts"] -N["auth.ts"] -U["frontendConfiguration.ts"] -14["highAvailability.ts"] -end -subgraph 8["data"] -9["variables.json"] -end -subgraph D["middleware"] -E["authMiddleware.ts"] -H["checkLock.ts"] -I["rateLimiter.ts"] -end -subgraph F["handlers"] -G["response.ts"] -M["auth.ts"] -Q["data.ts"] -T["frontend.ts"] -X["api.ts"] -10["graph.ts"] -13["ha.ts"] -19["notification.ts"] -1C["conf.ts"] -end -subgraph J["routes"] -subgraph K["auth"] -L["routes.ts"] -end -subgraph O["data"] -P["routes.ts"] -end -subgraph R["frontendController"] -S["routes.ts"] -end -subgraph V["getter"] -W["routes.ts"] -end -subgraph Y["graphs"] -Z["routes.ts"] -end -subgraph 11["highavailability"] -12["routes.ts"] -end -subgraph 17["notifications"] -18["routes.ts"] -end -subgraph 1A["setter"] -1B["routes.ts"] -end -end -subgraph 15["typings"] -16["ha.ts"] -end -end -1-->2 -2-->4 -2-->6 -2-->A -2-->E -2-->H -2-->I -2-->L -2-->P -2-->S -2-->W -2-->Z -2-->12 -2-->18 -2-->1B -6-->7 -7-->9 -A-->B -A-->C -C-->B -E-->G -H-->G -L-->M -M-->N -M-->G -P-->Q -Q-->B -Q-->G -S-->T -T-->U -T-->G -W-->X -X-->A -X-->G -Z-->10 -Z-->G -12-->13 -13-->14 -13-->G -14-->7 -14-->16 -18-->19 -19-->G -1B-->1C -1C-->A -1C-->G diff --git a/src/misc/dependencyGraphs/mermaid-api.txt b/src/misc/dependencyGraphs/mermaid-api.txt deleted file mode 100644 index 3cb4811e..00000000 --- a/src/misc/dependencyGraphs/mermaid-api.txt +++ /dev/null @@ -1,26 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["getter"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["api.ts"] -B["response.ts"] -end -subgraph 6["controllers"] -7["scheduler.ts"] -A["fetchData.ts"] -end -subgraph 8["config"] -9["db.ts"] -end -end -3-->5 -5-->7 -5-->B -7-->9 -7-->A -A-->9 diff --git a/src/misc/dependencyGraphs/mermaid-auth.txt b/src/misc/dependencyGraphs/mermaid-auth.txt deleted file mode 100644 index 336ddedb..00000000 --- a/src/misc/dependencyGraphs/mermaid-auth.txt +++ /dev/null @@ -1,19 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["auth"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["auth.ts"] -8["response.ts"] -end -subgraph 6["controllers"] -7["auth.ts"] -end -end -3-->5 -5-->7 -5-->8 diff --git a/src/misc/dependencyGraphs/mermaid-conf.txt b/src/misc/dependencyGraphs/mermaid-conf.txt deleted file mode 100644 index 370dd892..00000000 --- a/src/misc/dependencyGraphs/mermaid-conf.txt +++ /dev/null @@ -1,26 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["setter"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["conf.ts"] -B["response.ts"] -end -subgraph 6["controllers"] -7["scheduler.ts"] -A["fetchData.ts"] -end -subgraph 8["config"] -9["db.ts"] -end -end -3-->5 -5-->7 -5-->B -7-->9 -7-->A -A-->9 diff --git a/src/misc/dependencyGraphs/mermaid-data.txt b/src/misc/dependencyGraphs/mermaid-data.txt deleted file mode 100644 index 4aa6a133..00000000 --- a/src/misc/dependencyGraphs/mermaid-data.txt +++ /dev/null @@ -1,19 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["data"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["data.ts"] -8["response.ts"] -end -subgraph 6["config"] -7["db.ts"] -end -end -3-->5 -5-->7 -5-->8 diff --git a/src/misc/dependencyGraphs/mermaid-frontend.txt b/src/misc/dependencyGraphs/mermaid-frontend.txt deleted file mode 100644 index 8dde5ce9..00000000 --- a/src/misc/dependencyGraphs/mermaid-frontend.txt +++ /dev/null @@ -1,19 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["frontendController"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["frontend.ts"] -8["response.ts"] -end -subgraph 6["controllers"] -7["frontendConfiguration.ts"] -end -end -3-->5 -5-->7 -5-->8 diff --git a/src/misc/dependencyGraphs/mermaid-graph.txt b/src/misc/dependencyGraphs/mermaid-graph.txt deleted file mode 100644 index 34484535..00000000 --- a/src/misc/dependencyGraphs/mermaid-graph.txt +++ /dev/null @@ -1,15 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["graphs"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["graph.ts"] -6["response.ts"] -end -end -3-->5 -3-->6 diff --git a/src/misc/dependencyGraphs/mermaid-ha.txt b/src/misc/dependencyGraphs/mermaid-ha.txt deleted file mode 100644 index 2c789f6c..00000000 --- a/src/misc/dependencyGraphs/mermaid-ha.txt +++ /dev/null @@ -1,31 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["highavailability"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["ha.ts"] -E["response.ts"] -end -subgraph 6["controllers"] -7["highAvailability.ts"] -end -subgraph 8["config"] -9["variables.ts"] -end -subgraph A["data"] -B["variables.json"] -end -subgraph C["typings"] -D["ha.ts"] -end -end -3-->5 -5-->7 -5-->E -7-->9 -7-->D -9-->B diff --git a/src/misc/dependencyGraphs/mermaid-notificationService.txt b/src/misc/dependencyGraphs/mermaid-notificationService.txt deleted file mode 100644 index 2bc9731c..00000000 --- a/src/misc/dependencyGraphs/mermaid-notificationService.txt +++ /dev/null @@ -1,15 +0,0 @@ -flowchart TB - -subgraph 0["src"] -subgraph 1["routes"] -subgraph 2["notifications"] -3["routes.ts"] -end -end -subgraph 4["handlers"] -5["notification.ts"] -6["response.ts"] -end -end -3-->5 -5-->6 diff --git a/src/misc/entrypoint.sh b/src/misc/entrypoint.sh deleted file mode 100755 index b352ca75..00000000 --- a/src/misc/entrypoint.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -VERSION="$(cat ./package.json | grep version | cut -d '"' -f 4)" - -if [[ "$1" = "--dev" ]]; then - node_env="development" -elif [[ "$1" = "--prod" ]]; then - node_env="production" -fi - -echo -e " -\033[1;32mWelcome to\033[0m - -\033[1;34m###### ###### #### ### ### #### ######### ###### #########\033[0m -\033[1;34m### ### ### ### ### ### ### ### ### ### ### ###\033[0m -\033[1;34m### ### ### ### ### ###### #### ### ### ### ###\033[0m -\033[1;34m### ### ### ### ### ### ### #### ### ############ ###\033[0m -\033[1;34m### ### ### ### ### ### ### #### ### ### ### ###\033[0m -\033[1;34m###### ###### #### ### ### #### ### ### ### ### \033[0m(\033[1;33mAPI - v${VERSION}\033[0m) - -\033[1;36mUseful links:\033[0m - -- Documentation: \033[1;32mhttps://outline.itsnik.de/s/dockstat\033[0m -- GitHub (Frontend): \033[1;32mhttps://github.com/its4nik/dockstat\033[0m -- GitHub (Backend): \033[1;32mhttps://github.com/its4nik/dockstatapi\033[0m - -\033[1;35mSummary:\033[0m - -DockStat and DockStatAPI are 2 fully OpenSource projects, DockStatAPI is a simple but extensible API which allows queries via a REST endpoint. - -" - -bash ./createEnvFile.sh - -NODE_ENV=${node_env} node src/server.js diff --git a/src/misc/minifyDist.sh b/src/misc/minifyDist.sh deleted file mode 100755 index 171ef095..00000000 --- a/src/misc/minifyDist.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -dist="$(pwd)/dist" - -run_script() { - npx uglifyjs --no-annotations --in-situ "$1" > /dev/null - echo "✔️ Minified : $(basename "$1")" -} - -if [ -d "$dist" ]; then - echo "::: Dist directory exists." -else - echo "::: Dist does not exist... Running npx tsc" - npx tsc -fi - -max_jobs=$(nproc) -job_count=0 - -for file in $(find "$dist" -type f -name "*.js"); do - run_script "$file" & - ((job_count++)) - - if ((job_count >= max_jobs)); then - wait - job_count=0 - fi -done - -wait - -echo - -if [[ $1 == "--build-only" ]]; then - exit 0 -fi - -node dist/server.js diff --git a/src/misc/removeUnusedDeps.sh b/src/misc/removeUnusedDeps.sh deleted file mode 100755 index 5e806df3..00000000 --- a/src/misc/removeUnusedDeps.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -echo "Creating unused dependency list" - -TMP="$(npx depcheck --ignores https,@typescript-eslint/eslint-plugin,@typescript-eslint/parser,license-checker,uglify-js,@types/supports-color,ipaddr.js,dependency-cruiser,tsx,@types/bcrypt,@types/express,@types/express-handlebars,@types/node,ts-node --quiet --oneline | tail -n 1 | tr -d '\n')" - -lines=$(echo -n "$TMP" | tr -s ' ' '\n' | wc -l) - -if ((lines == 0)); then - echo "No unused dependencies." -else - echo - echo "Removing these unused dependencies ($lines):" - for entry in $TMP; do - echo "$entry" - done - echo - - - read -n 1 -p "Delete unused dependencies? (y/n) " input - echo - - case $input in - Y|y) - COMMAND=$(echo "npm remove $TMP") - $COMMAND - exit 0 - ;; - *) - echo "Aborting" - exit 1 - ;; - esac -fi - -exit 0 diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts new file mode 100644 index 00000000..48ca11a5 --- /dev/null +++ b/src/plugins/example.plugin.ts @@ -0,0 +1,11 @@ +import { Plugin } from "~/core/plugins/plugin-manager"; + +export default { + name: "example-plugin", + onContainerStart: (containerInfo) => { + console.log(`Container started: ${containerInfo.id}`); + }, + onMetricsReceived: (metrics) => { + console.log("Received metrics:", metrics); + }, +} satisfies Plugin; diff --git a/src/routes/auth/routes.ts b/src/routes/auth/routes.ts deleted file mode 100644 index 03549bfa..00000000 --- a/src/routes/auth/routes.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Router, Request, Response } from "express"; -import { createAuthenticationHandler } from "../../handlers/auth"; - -const router = Router(); - -router.post("/enable", async (req: Request, res: Response): Promise => { - const password = req.query.password as string; - const handler = createAuthenticationHandler(req, res); - await handler.enable(password); -}); - -router.post("/disable", async (req: Request, res: Response): Promise => { - const password = req.query.password as string; - const handler = createAuthenticationHandler(req, res); - await handler.disable(password); -}); - -export default router; diff --git a/src/routes/container-logs.ts b/src/routes/container-logs.ts new file mode 100644 index 00000000..085b19e4 --- /dev/null +++ b/src/routes/container-logs.ts @@ -0,0 +1,11 @@ +import { Elysia } from "elysia"; + +export const logRoutes = new Elysia({ prefix: "/logs" }).ws("/:containerId", { + open(ws) { + const containerId = ws.data.params.containerId; + console.log(`New log connection for ${containerId}`); + }, + message(ws, message) { + ws.send(message); + }, +}); diff --git a/src/routes/data/routes.ts b/src/routes/data/routes.ts deleted file mode 100644 index 93c4610b..00000000 --- a/src/routes/data/routes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import express, { Request, Response } from "express"; -const router = express.Router(); -import { createDatabaseHandler } from "../../handlers/data"; - -router.get("/latest", (req: Request, res: Response) => { - const DatabaseHandler = createDatabaseHandler(req, res); - return DatabaseHandler.latest(); -}); - -router.get("/all", (req: Request, res: Response) => { - const DatabaseHandler = createDatabaseHandler(req, res); - return DatabaseHandler.all(); -}); - -router.delete("/clear", (req: Request, res: Response) => { - const DatabaseHandler = createDatabaseHandler(req, res); - return DatabaseHandler.clear(); -}); - -export default router; diff --git a/src/routes/docker.ts b/src/routes/docker.ts new file mode 100644 index 00000000..993ae386 --- /dev/null +++ b/src/routes/docker.ts @@ -0,0 +1,22 @@ +import { Elysia, t } from "elysia"; +import { dockerHostManager } from "../core/docker/host-manager"; + +export const dockerRoutes = new Elysia({ prefix: "/docker-hosts" }) + .post( + "/", + async ({ body }) => { + const { id, url } = body; + await dockerHostManager.connect(id, url); + return { success: true }; + }, + { + body: t.Object({ + id: t.String(), + url: t.String(), + pollInterval: t.Number(), + }), + }, + ) + .get("/", () => { + return Array.from(dockerHostManager.connections.keys()); + }); diff --git a/src/routes/frontendController/routes.ts b/src/routes/frontendController/routes.ts deleted file mode 100644 index 723afa47..00000000 --- a/src/routes/frontendController/routes.ts +++ /dev/null @@ -1,76 +0,0 @@ -import express from "express"; -const router = express.Router(); -import { createFrontendHandler } from "../../handlers/frontend"; - -router.post("/show/:containerName", async (req, res) => { - const FrontendHandler = createFrontendHandler(req, res); - const containerName = req.params.containerName; - return FrontendHandler.show(containerName); -}); - -router.post("/tag/:containerName/:tag", async (req, res) => { - const { containerName, tag } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.addTag(containerName, tag); -}); - -router.post("/pin/:containerName", async (req, res) => { - const { containerName } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.pin(containerName); -}); - -router.post("/add-link/:containerName/:link", async (req, res) => { - const { containerName, link } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.addLink(containerName, link); -}); - -router.post( - "/add-icon/:containerName/:icon/:useCustomIcon", - async (req, res) => { - const { containerName, icon, useCustomIcon } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.addIcon(containerName, icon, useCustomIcon); - }, -); - -/* - ____ _____ _ _____ _____ _____ -| _ \| ____| | | ____|_ _| ____| -| | | | _| | | | _| | | | _| -| |_| | |___| |___| |___ | | | |___ -|____/|_____|_____|_____| |_| |_____| -*/ - -router.delete("/hide/:containerName", async (req, res) => { - const { containerName } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.hide(containerName); -}); - -router.delete("/remove-tag/:containerName/:tag", async (req, res) => { - const { containerName, tag } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.removeTag(containerName, tag); -}); - -router.delete("/unpin/:containerName", async (req, res) => { - const { containerName } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.unPin(containerName); -}); - -router.delete("/remove-link/:containerName", async (req, res) => { - const { containerName } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.removeLink(containerName); -}); - -router.delete("/remove-icon/:containerName", async (req, res) => { - const { containerName } = req.params; - const FrontendHandler = createFrontendHandler(req, res); - return FrontendHandler.removeIcon(containerName); -}); - -export default router; diff --git a/src/routes/getter/routes.ts b/src/routes/getter/routes.ts deleted file mode 100644 index d08ae511..00000000 --- a/src/routes/getter/routes.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Router, Request, Response } from "express"; -import { createApiHandler } from "../../handlers/api"; -const router = Router(); - -router.get("/hosts", (req: Request, res: Response) => { - const ApiHandler = createApiHandler(req, res); - return ApiHandler.hosts(); -}); - -router.get("/system", (req: Request, res: Response) => { - const ApiHandler = createApiHandler(req, res); - return ApiHandler.system(); -}); - -router.get("/host/:hostName/stats", async (req: Request, res: Response) => { - const { hostName } = req.params; - const ApiHandler = createApiHandler(req, res); - return ApiHandler.hostStats(hostName); -}); - -router.get("/containers", async (req: Request, res: Response) => { - const ApiHandler = createApiHandler(req, res); - return ApiHandler.containers(); -}); - -router.get("/config", async (req: Request, res: Response) => { - const ApiHandler = createApiHandler(req, res); - return ApiHandler.config(); -}); - -router.get("/current-schedule", (req: Request, res: Response) => { - const ApiHandler = createApiHandler(req, res); - return ApiHandler.currentSchedule(); -}); - -router.get("/status", async (req: Request, res: Response) => { - const ApiHandler = createApiHandler(req, res); - return ApiHandler.status(); -}); - -router.get("/frontend-config", (req: Request, res: Response) => { - const ApiHandler = createApiHandler(req, res); - return ApiHandler.frontendConfig(); -}); - -export default router; diff --git a/src/routes/graphs/routes.ts b/src/routes/graphs/routes.ts deleted file mode 100644 index fcaa7983..00000000 --- a/src/routes/graphs/routes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Request, Response, Router } from "express"; -import { createResponseHandler } from "../../handlers/response"; -import path from "path"; -import { rateLimitedReadFile } from "../../utils/rateLimitFS"; -const router = Router(); - -router.get("/json", async (req: Request, res: Response) => { - const ResponseHandler = createResponseHandler(res); - try { - const data = await rateLimitedReadFile( - path.join(__dirname, "/../../.." + "/src/data/graph.json"), - ); - return ResponseHandler.rawData(data, "Graph JSON fetched"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - return ResponseHandler.critical(errorMsg); - } -}); - -export default router; diff --git a/src/routes/highavailability/routes.ts b/src/routes/highavailability/routes.ts deleted file mode 100644 index d4adc466..00000000 --- a/src/routes/highavailability/routes.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Router, Request, Response } from "express"; -import { SyncRequestBody } from "../../typings/syncRequestBody"; -import { createHaHandler } from "../../handlers/ha"; -const router = Router(); - -router.get("/config", async (req: Request, res: Response) => { - const HaHandler = createHaHandler(req, res); - return HaHandler.config(); -}); - -router.post( - "/sync", - async ( - req: Request<{}, {}, SyncRequestBody>, // eslint-disable-line - res: Response, - ): Promise => { - const HaHandler = createHaHandler(req, res); - return HaHandler.sync(req); - }, -); - -router.get("/prepare-sync", async (req: Request, res: Response) => { - const HaHandler = createHaHandler(req, res); - return HaHandler.prepare(); -}); - -export default router; diff --git a/src/routes/logs.ts b/src/routes/logs.ts new file mode 100644 index 00000000..c4160001 --- /dev/null +++ b/src/routes/logs.ts @@ -0,0 +1,30 @@ +import { Elysia } from "elysia"; +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; + +export const backendLogs = new Elysia({ prefix: "/logs" }) + .get("/", async ({ set }) => { + try { + const logs = dbFunctions.getAllLogs(); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Retrieved all logs`); + return logs; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve logs,", error); + return { error: "Failed to retrieve logs" }; + } + }) + + .get("/:level", async ({ params: { level }, set }) => { + try { + const logs = dbFunctions.getLogsByLevel(level); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Retrieved logs (level: ${level})`); + return logs; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve logs"); + return { error: "Failed to retrieve logs" }; + } + }); diff --git a/src/routes/notifications/routes.ts b/src/routes/notifications/routes.ts deleted file mode 100644 index 13b754bd..00000000 --- a/src/routes/notifications/routes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Request, Response, Router } from "express"; -import { createNotificationHandler } from "../../handlers/notification"; -const router = Router(); - -router.get("/get-template", (req: Request, res: Response) => { - const NotificationHandler = createNotificationHandler(req, res); - return NotificationHandler.getTemplate(); -}); - -router.post("/set-template", (req: Request, res: Response): void => { - const NotificationHandler = createNotificationHandler(req, res); - return NotificationHandler.setTemplate(req); -}); - -router.post("/test/:type/:containerId", async (req: Request, res: Response) => { - const NotificationHandler = createNotificationHandler(req, res); - NotificationHandler.test(req); -}); - -export default router; diff --git a/src/routes/setter/routes.ts b/src/routes/setter/routes.ts deleted file mode 100644 index 16150293..00000000 --- a/src/routes/setter/routes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import express, { Router, Request, Response } from "express"; -const router: Router = express.Router(); -import { createConfHandler } from "../../handlers/conf"; - -router.put("/addHost", async (req: Request, res: Response): Promise => { - const ConfHandler = createConfHandler(req, res); - return ConfHandler.addHost(req); -}); - -router.delete("/removeHost", (req: Request, res: Response): void => { - const ConfHandler = createConfHandler(req, res); - return ConfHandler.removeHost(req); -}); - -router.put("/scheduler", (req: Request, res: Response) => { - const ConfHandler = createConfHandler(req, res); - return ConfHandler.scheduler(req); -}); - -export default router; diff --git a/src/routes/stack/routes.ts b/src/routes/stack/routes.ts deleted file mode 100644 index 8f9b9ae8..00000000 --- a/src/routes/stack/routes.ts +++ /dev/null @@ -1,35 +0,0 @@ -import express, { Router, Request, Response } from "express"; -const router: Router = express.Router(); -import { createStackHandler } from "../../handlers/stack"; - -router.post("/create/:name", async (req: Request, res: Response) => { - const StackHandler = createStackHandler(req, res); - return StackHandler.createStack(req, res); -}); - -router.post("/start/:name", async (req: Request, res: Response) => { - const StackHandler = createStackHandler(req, res); - return StackHandler.start(req, res); -}); - -router.post("/stop/:name", async (req: Request, res: Response) => { - const StackHandler = createStackHandler(req, res); - return StackHandler.stop(req, res); -}); - -router.get("/get/:name", async (req: Request, res: Response) => { - const StackHandler = createStackHandler(req, res); - return await StackHandler.stackCompose(req, res); -}); - -router.post("/set-env/:name", async (req: Request, res: Response) => { - const StackHandler = createStackHandler(req, res); - return await StackHandler.setStackEnv(req, res); -}); - -router.get("/get-env/:name", async (req: Request, res: Response) => { - const StackHandler = createStackHandler(req, res); - return await StackHandler.getStackEnv(req, res); -}); - -export default router; diff --git a/src/sample-variable.json b/src/sample-variable.json deleted file mode 100644 index f507796b..00000000 --- a/src/sample-variable.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "VERSION": "", - "RUNNING_IN_DOCKER": "", - "TRUSTED_PROXIES": "", - "HA_MASTER": "", - "HA_MASTER_IP": "", - "HA_NODE": "", - "HA_UNSAFE": "", - "DISCORD_WEBHOOK_URL": "", - "EMAIL_SENDER": "", - "EMAIL_RECIPIENT": "", - "EMAIL_PASSWORD": "", - "EMAIL_SERVICE": "", - "PUSHBULLET_ACCESS_TOKEN": "", - "PUSHOVER_USER_KEY": "", - "PUSHOVER_API_TOKEN": "", - "SLACK_WEBHOOK_URL": "", - "TELEGRAM_BOT_TOKEN": "", - "TELEGRAM_CHAT_ID": "", - "WHATSAPP_API_URL": "", - "WHATSAPP_RECIPIENT": "", - "AUTOMATIC_ENVIRONMENT_FILE_MANAGEMENT": "true", - "LOG_LEVEL": "info" -} diff --git a/src/server.ts b/src/server.ts deleted file mode 100644 index edcb2ec5..00000000 --- a/src/server.ts +++ /dev/null @@ -1,18 +0,0 @@ -import express from "express"; -import initializeApp from "./init"; -import writeUserConf from "./config/hostsystem"; -import { startServer } from "./utils/startServer"; -import http from "http"; - -const port: number = parseInt(process.env.PORT || "9876"); -const app = express(); -const server = http.createServer(app); - -initializeApp(app, server); - -if (process.env.NODE_ENV !== "testing") { - writeUserConf(port); - startServer(app, server, port); -} - -export default app; \ No newline at end of file diff --git a/src/typings/atomicWrite.ts b/src/typings/atomicWrite.ts deleted file mode 100644 index 1f4bfb4a..00000000 --- a/src/typings/atomicWrite.ts +++ /dev/null @@ -1,6 +0,0 @@ -interface AtomicWriteOptions { - mode?: number; - exclusive?: boolean; -} - -export { AtomicWriteOptions }; diff --git a/src/typings/dockerCompose.ts b/src/typings/dockerCompose.ts deleted file mode 100644 index e30f7e0d..00000000 --- a/src/typings/dockerCompose.ts +++ /dev/null @@ -1,92 +0,0 @@ -export interface DockerComposeFile { - services: Record; - networks?: Record; - volumes?: Record; -} - -export interface ServiceDefinition { - image?: string; - build?: BuildDefinition; - container_name?: string; - command?: string | string[]; - environment?: Record; - ports?: string[] | PortMapping[]; - volumes?: string[]; - networks?: string[]; - restart?: string; - depends_on?: string[]; - deploy?: DeployDefinition; - env_file?: string[]; -} - -export interface BuildDefinition { - context: string; - dockerfile?: string; - args?: Record; - cache_from?: string[]; - labels?: Record; - target?: string; -} - -export interface PortMapping { - target: number; - published: number; - protocol?: "tcp" | "udp"; - mode?: "host" | "ingress"; -} - -export interface DeployDefinition { - replicas?: number; - resources?: ResourcesDefinition; - restart_policy?: RestartPolicyDefinition; - labels?: Record; - update_config?: UpdateConfigDefinition; -} - -export interface ResourcesDefinition { - limits?: ResourceLimits; - reservations?: ResourceReservations; -} - -export interface ResourceLimits { - cpus?: string; - memory?: string; -} - -export interface ResourceReservations { - cpus?: string; - memory?: string; -} - -export interface RestartPolicyDefinition { - condition?: "none" | "on-failure" | "any"; - delay?: string; - max_attempts?: number; - window?: string; -} - -export interface UpdateConfigDefinition { - parallelism?: number; - delay?: string; - failure_action?: "continue" | "pause"; - monitor?: string; - max_failure_ratio?: number; - order?: "start-first" | "stop-first"; -} - -export interface NetworkDefinition { - driver?: string; - driver_opts?: Record; - attachable?: boolean; - external?: boolean; - internal?: boolean; - labels?: Record; -} - -export interface VolumeDefinition { - driver?: string; - driver_opts?: Record; - external?: boolean; - labels?: Record; - name?: string; -} diff --git a/src/typings/dockerConfig.ts b/src/typings/dockerConfig.ts deleted file mode 100644 index a1749d1f..00000000 --- a/src/typings/dockerConfig.ts +++ /dev/null @@ -1,35 +0,0 @@ -interface target { - name: string; - url: string; - port: number; -} - -interface dockerConfig { - hosts: target[]; -} - -interface HostConfig { - name: string; - [key: string]: string | number; -} - -interface ContainerData { - name: string; - id: string; - hostName: string; - state: string; - cpu_usage: number; - mem_usage: number; - mem_limit: number; - net_rx: number; - net_tx: number; - current_net_rx: number; - current_net_tx: number; - networkMode: string; -} - -interface AllContainerData { - [hostName: string]: ContainerData[] | { error: string }; -} - -export { dockerConfig, target, ContainerData, AllContainerData, HostConfig }; diff --git a/src/typings/dockerStackEnv.ts b/src/typings/dockerStackEnv.ts deleted file mode 100644 index c784b85d..00000000 --- a/src/typings/dockerStackEnv.ts +++ /dev/null @@ -1,10 +0,0 @@ -interface dockerStackProperty { - name: string; - value: string; -} - -interface dockerStackEnv { - environment: dockerStackProperty[]; -} - -export { dockerStackEnv, dockerStackProperty }; diff --git a/src/typings/frontendConfig.ts b/src/typings/frontendConfig.ts deleted file mode 100644 index 6ce14979..00000000 --- a/src/typings/frontendConfig.ts +++ /dev/null @@ -1,12 +0,0 @@ -interface Container { - name: string; - hidden?: boolean; - tags?: string[]; - link?: string; - icon?: string; - pinned?: boolean; -} - -type FrontendConfig = Container[]; - -export { FrontendConfig }; diff --git a/src/typings/ha.ts b/src/typings/ha.ts deleted file mode 100644 index f0352fc0..00000000 --- a/src/typings/ha.ts +++ /dev/null @@ -1,20 +0,0 @@ -interface HighAvailabilityConfig { - active: boolean; - master: boolean; - nodes: string[]; -} - -interface Node { - ip: string; - port: number; -} - -interface HaNodeConfig { - master: string; -} - -interface NodeCache { - [nodes: string]: Node; -} - -export { HighAvailabilityConfig, Node, HaNodeConfig, NodeCache }; diff --git a/src/typings/hostData.ts b/src/typings/hostData.ts deleted file mode 100644 index cf5a78da..00000000 --- a/src/typings/hostData.ts +++ /dev/null @@ -1,26 +0,0 @@ -interface Component { - Name: string; - Version: string; -} - -interface JsonData { - hostName: string; - info: { - ID: string; - Containers: number; - ContainersRunning: number; - ContainersPaused: number; - ContainersStopped: number; - Images: number; - OperatingSystem: string; - KernelVersion: string; - Architecture: string; - MemTotal: number; - NCPU: number; - }; - version: { - Components: Component[]; - }; -} - -export { JsonData }; diff --git a/src/typings/response.ts b/src/typings/response.ts deleted file mode 100644 index b122dfe2..00000000 --- a/src/typings/response.ts +++ /dev/null @@ -1,6 +0,0 @@ -interface StatusResponse { - ApiReachable: boolean; - online: { [key: string]: boolean }; -} - -export { StatusResponse }; diff --git a/src/typings/stackConfig.ts b/src/typings/stackConfig.ts deleted file mode 100644 index 45c72553..00000000 --- a/src/typings/stackConfig.ts +++ /dev/null @@ -1,5 +0,0 @@ -interface stackConfig { - stacks: string[]; -} - -export { stackConfig }; diff --git a/src/typings/states.ts b/src/typings/states.ts deleted file mode 100644 index d5eed20b..00000000 --- a/src/typings/states.ts +++ /dev/null @@ -1,10 +0,0 @@ -interface Container { - name: string; - id: string; - state: string; - hostName: string; -} - -type ContainerStates = Container[]; - -export { ContainerStates, Container }; diff --git a/src/typings/syncRequestBody.ts b/src/typings/syncRequestBody.ts deleted file mode 100644 index 36fd70a4..00000000 --- a/src/typings/syncRequestBody.ts +++ /dev/null @@ -1,5 +0,0 @@ -interface SyncRequestBody { - files: Record; -} - -export { SyncRequestBody }; diff --git a/src/typings/table.ts b/src/typings/table.ts deleted file mode 100644 index cf0c18ab..00000000 --- a/src/typings/table.ts +++ /dev/null @@ -1,11 +0,0 @@ -type Table = { - id: number; // Primary key, auto-incremented - info: string; // Non-null text field - timestamp: string; // ISO 8601 formatted datetime string -}; - -interface DataRow { - info: string; -} - -export { Table, DataRow }; diff --git a/src/typings/template.ts b/src/typings/template.ts deleted file mode 100644 index 71e0c8a3..00000000 --- a/src/typings/template.ts +++ /dev/null @@ -1,5 +0,0 @@ -interface TemplateData { - text: string; -} - -export { TemplateData }; diff --git a/src/utils/assets/api-icon.svg b/src/utils/assets/api-icon.svg deleted file mode 100644 index 5a4fdb7c..00000000 --- a/src/utils/assets/api-icon.svg +++ /dev/null @@ -1 +0,0 @@ -\ diff --git a/src/utils/assets/container-icon.svg b/src/utils/assets/container-icon.svg deleted file mode 100644 index 15ed98c6..00000000 --- a/src/utils/assets/container-icon.svg +++ /dev/null @@ -1 +0,0 @@ -\ diff --git a/src/utils/assets/server-icon.svg b/src/utils/assets/server-icon.svg deleted file mode 100644 index 31c92d4a..00000000 --- a/src/utils/assets/server-icon.svg +++ /dev/null @@ -1 +0,0 @@ -\ diff --git a/src/utils/atomicWrite.ts b/src/utils/atomicWrite.ts deleted file mode 100644 index d279475e..00000000 --- a/src/utils/atomicWrite.ts +++ /dev/null @@ -1,35 +0,0 @@ -import fs from "fs"; -import logger from "./logger"; -import { AtomicWriteOptions } from "../typings/atomicWrite"; - -export function atomicWrite( - targetPath: string, - data: object | string | Buffer | Record, - options: AtomicWriteOptions = {}, -): void { - const { mode = 0o600, exclusive = false } = options; - const tempFile = `${targetPath}.tmp`; - - try { - const writeData = - typeof data === "object" && !(data instanceof Buffer) - ? JSON.stringify(data, null, 2) - : data; - - if (exclusive && fs.existsSync(targetPath)) { - throw new Error(`File already exists: ${targetPath}`); - } - - fs.writeFileSync(tempFile, writeData, { mode }); - - fs.renameSync(tempFile, targetPath); - - logger.debug(`File successfully written to: ${targetPath}`); - } catch (error: unknown) { - if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); - logger.error( - `Failed to write file at ${targetPath}: ${(error as Error).message}`, - ); - throw error; - } -} diff --git a/src/utils/connectionChecker.ts b/src/utils/connectionChecker.ts deleted file mode 100644 index 5a45505b..00000000 --- a/src/utils/connectionChecker.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as fs from "fs"; -import * as net from "net"; -import logger from "./logger"; -import { target } from "../typings/dockerConfig"; -import { StatusResponse } from "../typings/response"; - -const filePath: string = "./src/data/dockerConfig.json"; - -async function checkHostStatus(hosts: target[]): Promise { - const results: { [key: string]: boolean } = {}; - for (const host of hosts) { - const { name, url, port } = host; - - const isOnline = await checkPort(url, port); - - results[name] = !!isOnline; - - if (results[name] == true) { - logger.debug(`${host.url}:${port} is online`); - } else { - logger.debug(`${host.url}:${port} is unreachable`); - } - } - - return { - ApiReachable: true, - online: results, - }; -} - -function checkPort(host: string, port: number): Promise { - return new Promise((resolve) => { - const socket = new net.Socket(); - socket.setTimeout(3000); - - socket.on("connect", () => { - socket.end(); - resolve(true); - }); - - socket.on("timeout", () => { - socket.destroy(); - resolve(false); - }); - - socket.on("error", () => { - socket.destroy(); - resolve(false); - }); - - socket.connect(port, host); - }); -} - -async function checkReachability(): Promise { - try { - const data = fs.readFileSync(filePath, "utf-8"); - const parsedData = JSON.parse(data); - const hosts: target[] = parsedData.hosts; - return await checkHostStatus(hosts); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -export default checkReachability; diff --git a/src/utils/containerService.ts b/src/utils/containerService.ts deleted file mode 100644 index 0bb0a4e7..00000000 --- a/src/utils/containerService.ts +++ /dev/null @@ -1,173 +0,0 @@ -import logger from "./logger"; -import { ContainerInfo } from "dockerode"; -import { getDockerClient } from "./dockerClient"; -import fs from "fs"; -import { atomicWrite } from "./atomicWrite"; -const configPath = "./src/data/dockerConfig.json"; -import { AllContainerData, HostConfig } from "../typings/dockerConfig"; -import { generateGraphJSON } from "../handlers/graph"; -import { WebSocket } from "ws"; - -export function loadConfig() { - try { - if (!fs.existsSync(configPath)) { - logger.warn( - `Config file not found. Creating an empty file at ${configPath}`, - ); - atomicWrite(configPath, JSON.stringify({ hosts: [] }, null, 2)); - } - - const configData = fs.readFileSync(configPath, "utf-8"); - logger.debug("Loaded " + configPath); - return JSON.parse(configData); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return { hosts: [] }; - } -} - -export async function fetchContainersForHost(hostName: string) { - const config = loadConfig(); - const hostConfig = config.hosts.find((h: HostConfig) => h.name === hostName); - - if (!hostConfig) { - throw new Error(`Host ${hostName} not found in configuration`); - } - - try { - const docker = getDockerClient(hostName); - const containers: ContainerInfo[] = await docker.listContainers({ - all: true, - }); - - return await Promise.all( - containers.map(async (container) => { - try { - const containerInstance = docker.getContainer(container.Id); - const [containerInfo, containerStats] = await Promise.all([ - containerInstance.inspect(), - containerInstance.stats({ stream: false }), - ]); - - const cpuDelta = - containerStats.cpu_stats.cpu_usage.total_usage - - containerStats.precpu_stats.cpu_usage.total_usage; - const systemCpuDelta = - containerStats.cpu_stats.system_cpu_usage - - containerStats.precpu_stats.system_cpu_usage; - const cpuUsage = - systemCpuDelta > 0 - ? (cpuDelta / systemCpuDelta) * - containerStats.cpu_stats.online_cpus - : 0; - - return { - name: container.Names[0].replace("/", ""), - id: container.Id, - hostName, - state: container.State, - cpu_usage: cpuUsage, - mem_usage: containerStats.memory_stats.usage, - mem_limit: containerStats.memory_stats.limit, - net_rx: containerStats.networks?.eth0?.rx_bytes || 0, - net_tx: containerStats.networks?.eth0?.tx_bytes || 0, - current_net_rx: containerStats.networks?.eth0?.rx_bytes || 0, - current_net_tx: containerStats.networks?.eth0?.tx_bytes || 0, - networkMode: containerInfo.HostConfig.NetworkMode || "unknown", - }; - } catch (error) { - logger.error(`Error processing container ${container.Id}: ${error}`); - return { - name: container.Names[0].replace("/", ""), - id: container.Id, - hostName, - state: container.State, - cpu_usage: 0, - mem_usage: 0, - mem_limit: 0, - net_rx: 0, - net_tx: 0, - current_net_rx: 0, - current_net_tx: 0, - networkMode: "unknown", - }; - } - }), - ); - } catch (error) { - logger.error(`Error fetching containers for ${hostName}: ${error}`); - throw error; - } -} - -export async function fetchAllContainers(): Promise { - const config = loadConfig(); - const allContainerData: AllContainerData = {}; - - await Promise.all( - config.hosts.map(async (hostConfig: HostConfig) => { - try { - allContainerData[hostConfig.name] = await fetchContainersForHost( - hostConfig.name, - ); - } catch (error) { - allContainerData[hostConfig.name] = { - error: `Error fetching containers: ${error instanceof Error ? error.message : String(error)}`, - }; - } - }), - ); - - generateGraphJSON(allContainerData); - return allContainerData; -} - -export async function streamContainerData(ws: WebSocket, hostName: string) { - try { - const containers = await fetchContainersForHost(hostName); - ws.send(JSON.stringify({ type: "containers", data: containers })); - - const docker = getDockerClient(hostName); - const eventStream = await docker.getEvents(); - - // eslint-disable-next-line - if (!(eventStream instanceof require("stream").Readable)) { - throw new Error("Failed to get valid event stream"); - } - - const handleData = (chunk: Buffer) => { - ws.send( - JSON.stringify({ type: "container-event", data: chunk.toString() }), - ); - }; - - const handleError = (err: Error) => { - logger.error(`Event stream error for ${hostName}: ${err.message}`); - ws.close(); - }; - - eventStream.on("data", handleData).on("error", handleError); - - const closeHandler = () => { - eventStream - .removeListener("data", handleData) - .removeListener("error", handleError) - .removeListener("closed", handleError); - logger.info(`Closed event stream for ${hostName}`); - }; - - ws.on("close", closeHandler); - ws.on("error", closeHandler); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - logger.error("Container data error:", message); - ws.send( - JSON.stringify({ - error: "Failed to fetch container data", - details: message, - }), - ); - ws.close(); - } -} diff --git a/src/utils/dockerClient.ts b/src/utils/dockerClient.ts deleted file mode 100644 index ff770888..00000000 --- a/src/utils/dockerClient.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Docker from "dockerode"; -import fs from "fs"; -import logger from "./logger"; -import { dockerConfig, target } from "../typings/dockerConfig"; - -function loadDockerConfig(): dockerConfig { - const configPath = "./src/data/dockerConfig.json"; - try { - const rawData = fs.readFileSync(configPath, "utf-8"); - logger.debug("Refreshed DockerConfig.json"); - return JSON.parse(rawData) as dockerConfig; - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } -} - -function createDockerClient(hostConfig: target): Docker { - logger.info( - `Creating Docker client for host: ${hostConfig.url} on port: ${hostConfig.port || 2375}`, - ); - return new Docker({ - host: hostConfig.url, - port: hostConfig.port || 2375, - protocol: "http", - }); -} - -export const getDockerClient = (hostName: string): Docker => { - logger.debug(`Getting Docker Client for ${hostName}`); - const config = loadDockerConfig(); - const hostConfig = config.hosts.find((host) => host.name === hostName); - - if (!hostConfig) { - const errorMsg = `Docker host ${hostName} not found in configuration`; - logger.error(errorMsg); - throw new Error(errorMsg); - } - return createDockerClient(hostConfig); -}; diff --git a/src/utils/extractHostData.ts b/src/utils/extractHostData.ts deleted file mode 100644 index 992f9638..00000000 --- a/src/utils/extractHostData.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { JsonData } from "../typings/hostData"; -import logger from "./logger"; - -type ComponentMap = Record; - -interface RelevantData { - hostName: string; - info: { - ID: string; - Containers: number; - ContainersRunning: number; - ContainersPaused: number; - ContainersStopped: number; - Images: number; - OperatingSystem: string; - KernelVersion: string; - Architecture: string; - MemTotal: number; - NCPU: number; - }; - version: { - Components: ComponentMap; - }; -} - -function processComponents(components: unknown): ComponentMap { - try { - if (!Array.isArray(components)) return {}; - - return components.reduce((acc, component) => { - if ( - typeof component === "object" && - component !== null && - "Name" in component && - "Version" in component - ) { - const { Name, Version } = component; - if (typeof Name === "string" && typeof Version === "string") { - acc[Name] = Version; - } - } - return acc; - }, {}); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.error(`Error processing components: ${errorMessage}`); - return {}; - } -} - -export function extractRelevantData(jsonData: JsonData): RelevantData { - return { - hostName: jsonData.hostName, - info: { - ID: jsonData.info.ID, - Containers: jsonData.info.Containers, - ContainersRunning: jsonData.info.ContainersRunning, - ContainersPaused: jsonData.info.ContainersPaused, - ContainersStopped: jsonData.info.ContainersStopped, - Images: jsonData.info.Images, - OperatingSystem: jsonData.info.OperatingSystem, - KernelVersion: jsonData.info.KernelVersion, - Architecture: jsonData.info.Architecture, - MemTotal: jsonData.info.MemTotal, - NCPU: jsonData.info.NCPU, - }, - version: { - Components: processComponents(jsonData?.version?.Components), - }, - }; -} - -export default extractRelevantData; diff --git a/src/utils/logger.ts b/src/utils/logger.ts deleted file mode 100644 index 2fd67bd5..00000000 --- a/src/utils/logger.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { createLogger, format, transports } from "winston"; -import DailyRotateFile from "winston-daily-rotate-file"; -import { LOG_LEVEL } from "../config/variables"; - -const colors = { - gray: "\x1b[90m", - reset: "\x1b[0m", - white: "\x1b[97m", - red: "\x1b[31m", - green: "\x1b[32m", - yellow: "\x1b[33m", - blue: "\x1b[34m", -}; - -function colorizeLogLevel(level: string, levelName: string) { - switch (level) { - case "info": - return `${colors.green}${levelName}${colors.reset}`; - case "debug": - return `${colors.blue}${levelName}${colors.reset}`; - case "error": - return `${colors.red}${levelName}${colors.reset}`; - case "warn": - return `${colors.yellow}${levelName}${colors.reset}`; - default: - return `${colors.gray}UNKNOWN${colors.reset}`; - } -} - -// Filter out Exit listeners logs -const filterLogs = format((info) => { - if ( - typeof info.message === "string" && - info.message.includes("Exit listeners detected") - ) { - return false; - } - return info; -}); - -const logger = createLogger({ - level: LOG_LEVEL, - format: format.combine( - filterLogs(), - format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), - ), - transports: [ - new transports.Console({ - format: format.combine( - format.printf((info) => { - const level = info.level.toUpperCase().padEnd(5, " "); - const timestamp = `${colors.gray}${info.timestamp}${colors.reset}`; - const levelColorized = colorizeLogLevel( - info.level.toLowerCase(), - level, - ); - const message = `${colors.white}${(info.message as string).replace(/\n|\r/g, "")}${colors.reset}`; - - return `${timestamp} ${levelColorized} : ${message}`; - }), - ), - }), - new DailyRotateFile({ - filename: "logs/app-%DATE%.log", - datePattern: "YYYY-MM-DD", - maxSize: "20m", - maxFiles: "14d", - zippedArchive: true, - format: format.combine( - format.printf((info) => { - const level = info.level.toUpperCase().padEnd(5, " "); - return `${info.timestamp} ${level} : ${info.message}`; - }), - ), - }), - ], -}); - -export default logger; diff --git a/src/utils/notifications/_notify.ts b/src/utils/notifications/_notify.ts deleted file mode 100644 index 49717f90..00000000 --- a/src/utils/notifications/_notify.ts +++ /dev/null @@ -1,51 +0,0 @@ -import logger from "../../utils/logger"; -import { telegramNotification } from "./telegram"; -import { slackNotification } from "./slack"; -import { discordNotification } from "./discord"; -import { emailNotification } from "./email"; -import { whatsappNotification } from "./whatsapp"; -import { pushbulletNotification } from "./pushbullet"; -import { pushoverNotification } from "./pushover"; - -async function notify(type: string, containerId: string) { - if (!containerId) { - logger.error("Container ID is required."); - throw new Error("Container ID is required."); - } - - switch (type) { - case "telegram": - logger.debug("Sending Telegram notification..."); - await telegramNotification(containerId); - break; - case "slack": - logger.debug("Sending Slack notification..."); - await slackNotification(containerId); - break; - case "discord": - logger.debug("Sending Discord notification..."); - await discordNotification(containerId); - break; - case "email": - logger.debug("Sending Email notification..."); - await emailNotification(containerId); - break; - case "whatsapp": - logger.debug("Sending WhatsApp notification..."); - await whatsappNotification(containerId); - break; - case "pushbullet": - logger.debug("Sending Pushbullet notification..."); - await pushbulletNotification(containerId); - break; - case "pushover": - logger.debug("Sending Pushover notification..."); - await pushoverNotification(containerId); - break; - default: - logger.error("Unknown notification type."); - throw new Error("Unknown notification type."); - } -} - -export default notify; diff --git a/src/utils/notifications/_template.ts b/src/utils/notifications/_template.ts deleted file mode 100644 index fd5d71ed..00000000 --- a/src/utils/notifications/_template.ts +++ /dev/null @@ -1,76 +0,0 @@ -import fs from "fs"; -import logger from "../logger"; -import { ContainerStates, Container } from "../../typings/states"; - -const templatePath: string = "./src/data/template.json"; -const containersPath: string = "./src/data/states.json"; - -interface Template { - text: string; -} - -function getTemplate(): Template | null { - try { - const data = fs.readFileSync(templatePath, "utf8"); - return JSON.parse(data); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return null; - } -} - -function setTemplate(newTemplate: string): void { - try { - fs.writeFileSync( - templatePath, - JSON.stringify({ text: newTemplate }, null, 2), - "utf8", - ); - logger.debug("Template updated successfully"); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} - -function renderTemplate(containerId: string): string | null { - const template = getTemplate(); - if (!template) { - logger.error("Template is missing or not a string"); - return null; - } - - try { - const data = fs.readFileSync(containersPath, "utf8"); - const containers = JSON.parse(data); - - let containerData: ContainerStates | null = null; - for (const host in containers) { - containerData = containers[host].find( - (c: Container) => c.id === containerId, - ); - if (containerData) { - break; - } - } - - if (!containerData) { - logger.error(`Container with ID ${containerId} not found`); - return null; - } - - // Substitute placeholders in the template with container data - return Object.keys(containerData).reduce((text, key) => { - const value = containerData[key as keyof ContainerStates]; - // Convert value to a string to avoid errors - return text.replace(new RegExp(`{{${key}}}`, "g"), String(value)); - }, template.text); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - return null; - } -} - -export { getTemplate, setTemplate, renderTemplate }; diff --git a/src/utils/notifications/discord.ts b/src/utils/notifications/discord.ts deleted file mode 100644 index d9be3a02..00000000 --- a/src/utils/notifications/discord.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as https from "https"; -import logger from "../logger"; -import { renderTemplate } from "./_template"; -import { DISCORD_WEBHOOK_URL } from "../../config/variables"; - -const discord_webhook_url: string = DISCORD_WEBHOOK_URL; - -export async function discordNotification(containerId: string): Promise { - const discord_message: string | null = renderTemplate(containerId); - if (!discord_message) { - logger.error("Failed to create notification message."); - return; - } - - if (!discord_webhook_url) { - logger.error("Discord webhook URL is not set."); - return; - } - - const postData = JSON.stringify({ - content: discord_message, - }); - - const url = new URL(discord_webhook_url); - - const options = { - hostname: url.hostname, - path: url.pathname, - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(postData), - }, - }; - - const req = https.request(options, (res) => { - let data = ""; - - res.on("data", (chunk) => { - data += chunk; - }); - - res.on("end", () => { - if (res.statusCode !== 200) { - logger.error(`Discord API error: ${data}`); - } - }); - }); - - req.on("error", (error) => { - logger.error("Error sending Discord message:", error); - }); - - req.write(postData); - req.end(); -} diff --git a/src/utils/notifications/email.ts b/src/utils/notifications/email.ts deleted file mode 100644 index 62b37d3a..00000000 --- a/src/utils/notifications/email.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { SendMailOptions, createTransport } from "nodemailer"; -import logger from "../logger"; -import { renderTemplate } from "./_template"; -import { - EMAIL_SENDER, - EMAIL_SERVICE, - EMAIL_PASSWORD, - EMAIL_RECIPIENT, -} from "../../config/variables"; - -const email_sender: string = EMAIL_SENDER; -const email_recipient: string = EMAIL_RECIPIENT; -const email_password: string = EMAIL_PASSWORD; -const email_service: string = EMAIL_SERVICE; - -export async function emailNotification(containerId: string) { - // Validate email configuration parameters - if (!email_sender || !email_recipient || !email_password || !email_service) { - logger.error( - "Email notification failed: Missing configuration parameters. " + - "Please ensure EMAIL_SENDER, EMAIL_RECIPIENT, EMAIL_PASSWORD, and EMAIL_SERVICE are set in environment variables.", - ); - return; - } - - const email_message: string | null = renderTemplate(containerId); - if (!email_message) { - logger.error("Failed to create notification message."); - return; - } - - const transporter = createTransport({ - service: email_service, - auth: { - user: email_sender, - pass: email_password, - }, - }); - - const mailOptions: SendMailOptions = { - from: email_sender, - to: email_recipient, - subject: "DockStat", - text: email_message, - }; - - try { - await transporter.sendMail(mailOptions); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - } -} diff --git a/src/utils/notifications/pushbullet.ts b/src/utils/notifications/pushbullet.ts deleted file mode 100644 index 811427a1..00000000 --- a/src/utils/notifications/pushbullet.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as https from "https"; -import logger from "../logger"; -import { renderTemplate } from "./_template"; -import { PUSHBULLET_ACCESS_TOKEN } from "../../config/variables"; - -const pushbullet_access_token: string = PUSHBULLET_ACCESS_TOKEN; - -export async function pushbulletNotification( - containerId: string, -): Promise { - const pushbullet_message: string | null = renderTemplate(containerId); - if (!pushbullet_message) { - logger.error("Failed to create notification message."); - return; - } - - if (!pushbullet_access_token) { - logger.error("Pushbullet access token is not set."); - return; - } - - const postData = JSON.stringify({ - type: "note", - title: "Container Notification", - body: pushbullet_message, - }); - - const options = { - hostname: "api.pushbullet.com", - path: "/v2/pushes", - method: "POST", - headers: { - "Access-Token": pushbullet_access_token, - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(postData), - }, - }; - - const req = https.request(options, (res) => { - let data = ""; - - res.on("data", (chunk) => { - data += chunk; - }); - - res.on("end", () => { - if (res.statusCode !== 200) { - logger.error(`Pushbullet API error: ${data}`); - } - }); - }); - - req.on("error", (error) => { - logger.error("Error sending Pushbullet message:", error); - }); - - req.write(postData); - req.end(); -} diff --git a/src/utils/notifications/pushover.ts b/src/utils/notifications/pushover.ts deleted file mode 100644 index aac71b3b..00000000 --- a/src/utils/notifications/pushover.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as https from "https"; -import logger from "../logger"; -import { renderTemplate } from "./_template"; -import { PUSHOVER_USER_KEY, PUSHOVER_API_TOKEN } from "../../config/variables"; - -const pushover_user_key: string = PUSHOVER_USER_KEY; -const pushover_api_token: string = PUSHOVER_API_TOKEN; - -export async function pushoverNotification(containerId: string): Promise { - const pushover_message: string | null = renderTemplate(containerId); - if (!pushover_message) { - logger.error("Failed to create notification message."); - return; - } - - if (!pushover_api_token || !pushover_user_key) { - logger.error("Pushover API token or user key is not set."); - return; - } - - const postData = new URLSearchParams({ - token: pushover_api_token, - user: pushover_user_key, - message: pushover_message, - }).toString(); - - const options = { - hostname: "api.pushover.net", - path: "/1/messages.json", - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "Content-Length": Buffer.byteLength(postData), - }, - }; - - const req = https.request(options, (res) => { - let data = ""; - - res.on("data", (chunk) => { - data += chunk; - }); - - res.on("end", () => { - if (res.statusCode !== 200) { - logger.error(`Pushover API error: ${data}`); - } - }); - }); - - req.on("error", (error) => { - logger.error("Error sending Pushover message:", error); - }); - - req.write(postData); - req.end(); -} diff --git a/src/utils/notifications/slack.ts b/src/utils/notifications/slack.ts deleted file mode 100644 index e1e7216b..00000000 --- a/src/utils/notifications/slack.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as https from "https"; -import logger from "../logger"; -import { renderTemplate } from "./_template"; -import { SLACK_WEBHOOK_URL } from "../../config/variables"; - -const slack_webhook_url: string = SLACK_WEBHOOK_URL; - -export async function slackNotification(containerId: string): Promise { - const slack_message: string | null = renderTemplate(containerId); - if (!slack_message) { - logger.error("Failed to create notification message."); - return; - } - - if (!slack_webhook_url) { - logger.error("Slack webhook URL is not set."); - return; - } - - const postData = JSON.stringify({ - text: slack_message, - }); - - const url = new URL(slack_webhook_url); - - const options = { - hostname: url.hostname, - path: url.pathname, - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(postData), - }, - }; - - const req = https.request(options, (res) => { - let data = ""; - - res.on("data", (chunk) => { - data += chunk; - }); - - res.on("end", () => { - if (res.statusCode !== 200) { - logger.error(`Slack API error: ${data}`); - } - }); - }); - - req.on("error", (error) => { - logger.error("Error sending Slack message:", error); - }); - - req.write(postData); - req.end(); -} diff --git a/src/utils/notifications/telegram.ts b/src/utils/notifications/telegram.ts deleted file mode 100644 index 440e0916..00000000 --- a/src/utils/notifications/telegram.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as https from "https"; -import logger from "../logger"; -import { renderTemplate } from "./_template"; -import { TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID } from "../../config/variables"; - -const telegram_bot_token: string = TELEGRAM_BOT_TOKEN; -const telegram_chat_id: string = TELEGRAM_CHAT_ID; - -export async function telegramNotification(containerId: string): Promise { - const telegram_message: string | null = renderTemplate(containerId); - if (!telegram_message) { - logger.error("Failed to create notification message."); - return; - } - - if (!telegram_bot_token || !telegram_chat_id) { - logger.error("Telegram bot token or chat ID is not set."); - return; - } - - const postData = JSON.stringify({ - chat_id: telegram_chat_id, - text: telegram_message, - }); - - const options = { - hostname: "api.telegram.org", - path: `/bot${telegram_bot_token}/sendMessage`, - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(postData), - }, - }; - - const req = https.request(options, (res) => { - let data = ""; - - res.on("data", (chunk) => { - data += chunk; - }); - - res.on("end", () => { - if (res.statusCode !== 200) { - logger.error(`Telegram API error: ${data}`); - } - }); - }); - - req.on("error", (error) => { - logger.error("Error sending message:", error); - }); - - req.write(postData); - req.end(); -} diff --git a/src/utils/notifications/whatsapp.ts b/src/utils/notifications/whatsapp.ts deleted file mode 100644 index 1eb7575e..00000000 --- a/src/utils/notifications/whatsapp.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as https from "https"; -import logger from "../logger"; -import { renderTemplate } from "./_template"; -import { WHATSAPP_API_URL, WHATSAPP_RECIPIENT } from "../../config/variables"; - -const whatsapp_api_url: string = WHATSAPP_API_URL; -const whatsapp_recipient: string = WHATSAPP_RECIPIENT; - -export async function whatsappNotification(containerId: string): Promise { - const whatsapp_message: string | null = renderTemplate(containerId); - if (!whatsapp_message) { - logger.error("Failed to create notification message."); - return; - } - - if (!whatsapp_api_url || !whatsapp_recipient) { - logger.error("WhatsApp API URL or recipient is not set."); - return; - } - - const postData = JSON.stringify({ - to: whatsapp_recipient, - body: whatsapp_message, - }); - - const url = new URL(whatsapp_api_url); - - const options = { - hostname: url.hostname, - path: url.pathname, - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(postData), - }, - }; - - const req = https.request(options, (res) => { - let data = ""; - - res.on("data", (chunk) => { - data += chunk; - }); - - res.on("end", () => { - if (res.statusCode !== 200) { - logger.error(`WhatsApp API error: ${data}`); - } - }); - }); - - req.on("error", (error) => { - logger.error("Error sending WhatsApp message:", error); - }); - - req.write(postData); - req.end(); -} diff --git a/src/utils/rateLimitFS.ts b/src/utils/rateLimitFS.ts deleted file mode 100644 index a8f0b42d..00000000 --- a/src/utils/rateLimitFS.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { promises as fs, existsSync } from "fs"; - -const delay = (ms: number): Promise => - new Promise((resolve) => setTimeout(resolve, ms)); - -let lastOperationTime = 0; -const rateLimitDuration = 500; - -export const rateLimitedReadFile = async ( - filePath: string, - encoding: BufferEncoding = "utf8", -): Promise => { - const now = Date.now(); - const timeSinceLastOperation = now - lastOperationTime; - - if (timeSinceLastOperation < rateLimitDuration) { - await delay(rateLimitDuration - timeSinceLastOperation); - } - - lastOperationTime = Date.now(); - return fs.readFile(filePath, encoding); -}; - -export const rateLimitedExistsSync = async ( - filePath: string, -): Promise => { - const now = Date.now(); - const timeSinceLastOperation = now - lastOperationTime; - - if (timeSinceLastOperation < rateLimitDuration) { - await delay(rateLimitDuration - timeSinceLastOperation); - } - - lastOperationTime = Date.now(); - return existsSync(filePath); -}; diff --git a/src/utils/startServer.ts b/src/utils/startServer.ts deleted file mode 100644 index 52dcc256..00000000 --- a/src/utils/startServer.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Express } from "express"; -import { Server } from "http"; -import { startMasterNode } from "../controllers/highAvailability"; -import writeUserConf from "../config/hostsystem"; -import initFiles from "../config/initFiles"; - -export function startServer(app: Express, server: Server, port: number) { - if (process.env.NODE_ENV === "testing") { - writeUserConf(port); - initFiles(); - } - - server.listen(port, () => { - startMasterNode(); - }); -} diff --git a/src/utils/swaggerDocs.ts b/src/utils/swaggerDocs.ts deleted file mode 100644 index 7ed90d9d..00000000 --- a/src/utils/swaggerDocs.ts +++ /dev/null @@ -1,12 +0,0 @@ -import swaggerUi from "swagger-ui-express"; -import { options } from "../config/swaggerConfig"; -import yaml from "yamljs"; -import express from "express"; -import { SwaggerDefinition } from "swagger-jsdoc"; - -const swaggerDocs = (app: express.Application) => { - const swaggerYaml: SwaggerDefinition = yaml.load("./src/config/swagger.yaml"); - app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerYaml, options)); -}; - -export default swaggerDocs; diff --git a/src/utils/webSocket.ts b/src/utils/webSocket.ts deleted file mode 100644 index 66d1f74b..00000000 --- a/src/utils/webSocket.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Server } from "http"; -import { WebSocketServer, WebSocket } from "ws"; -import { URL } from "url"; -import fs from "fs"; -import logger from "./logger"; -import { streamContainerData } from "./containerService"; - -export function setupWebSocket(server: Server) { - const wss = new WebSocketServer({ noServer: true }); - - server.on("upgrade", (req, socket, head) => { - logger.debug(`Received upgrade request for URL: ${req.url}`); - const baseURL = `http://${req.headers.host}/`; - const requestURL = new URL(req.url || "", baseURL); - const { pathname } = requestURL; - logger.debug(`Parsed pathname: ${pathname}`); - - // Debug log to verify path handling - logger.debug(`Handling upgrade for path: ${pathname}`); - - if (pathname === "/wss/container-data" || pathname === "/wss/server-logs") { - wss.handleUpgrade(req, socket, head, (ws) => { - wss.emit("connection", ws, req); - }); - } else { - logger.warn(`Rejected WebSocket connection to invalid path: ${pathname}`); - socket.write("HTTP/1.1 404 Not Found\r\n\r\n"); - socket.destroy(); - } - }); - - server.on("error", (error) => { - logger.error("HTTP server error:", error); - }); - - logger.debug("WebSocket server attached to HTTP server"); - - wss.on("connection", (ws: WebSocket, req) => { - const baseURL = `http://${req.headers.host}/`; - const requestURL = new URL(req.url || "", baseURL); - const { pathname } = requestURL; - - logger.info(`WebSocket connection established to ${pathname}`); - - const handleError = (error: string) => { - ws.send(JSON.stringify({ error })); - ws.close(); - }; - - if (pathname === "/wss/container-data") { - const hostName = requestURL.searchParams.get("host"); - if (!hostName) { - handleError("Missing required host parameter"); - return; - } - streamContainerData(ws, hostName); - } else if (pathname === "/wss/server-logs") { - const logFiles = fs - .readdirSync("logs/") - .filter((file) => file.startsWith("app-")); - - if (logFiles.length === 0) { - console.error("No log files found"); - return; - } - - const sortedLogFiles = logFiles.sort((a, b) => { - const dateA = a.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; - const dateB = b.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? ""; - - return dateB.localeCompare(dateA); - }); - - const logPath = "logs/" + sortedLogFiles[0]; - - if (!fs.existsSync(logPath)) { - handleError("Log file not found"); - logger.error(`Log file ${logPath} not found`); - return; - } - - // Read the initial content of the log file - const history = fs.readFileSync(logPath, "utf-8"); - ws.send(JSON.stringify({ type: "log-history", data: history })); - - // Watch the log file for changes - const watcher = fs.watchFile( - logPath, - { interval: 1000 }, - (curr, prev) => { - if (curr.size > prev.size) { - const stream = fs.createReadStream(logPath, { - start: prev.size, - end: curr.size - 1, - encoding: "utf-8", - }); - - stream.on("data", (chunk) => { - ws.send(JSON.stringify({ type: "log-update", data: chunk })); - }); - } - }, - ); - - ws.on("close", () => { - watcher.removeAllListeners(); - logger.info("Closed WebSocket connection for logs"); - }); - } else { - handleError("Invalid WebSocket endpoint"); - } - }); -} diff --git a/tsconfig.json b/tsconfig.json index c4f6f4c0..b95e7e02 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,107 @@ { "compilerOptions": { - "resolveJsonModule": true, - "target": "ES2020", - "outDir": "dist/src", - "module": "CommonJS", - "moduleResolution": "node", - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true - }, - "$schema": "https://json.schemastore.org/tsconfig", - "display": "Recommended", - "include": ["src/**/*", "**/*.d.ts", "__tests__/**/*"], - "exclude": ["node_modules", "**/*.spec.ts"] + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ES2022" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + "paths": { + "~/*": ["./src/*"] + } /* Specify a set of entries that re-map imports to additional lookup locations. */, + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": [ + "bun-types" + ] /* Specify type package names to be included without being referenced in a source file. */, + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } } From d01c12be4055a4611031f55aadd1271e93199874 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sun, 23 Feb 2025 21:34:05 +0100 Subject: [PATCH 135/369] Feat: Log deletion, first docker statistics, typechecker, and more! --- .dockerignore | 15 ++ bun.lock | 133 +++++++++++++++++ docker/Dockerfile | 3 + docker/docker-compose.dev.yaml | 52 +++++++ package.json | 9 +- src/core/database/repository.ts | 137 ++++++++++++++++-- src/core/utils/logger.ts | 8 +- src/core/utils/type-check.ts | 28 ++++ src/index.ts | 18 +-- src/routes/container-logs.ts | 11 -- src/routes/docker-manager.ts | 42 ++++++ src/routes/docker-stats.ts | 243 ++++++++++++++++++++++++++++++++ src/routes/docker.ts | 22 --- src/routes/logs.ts | 26 ++++ src/typings/docker.ts | 28 ++++ 15 files changed, 712 insertions(+), 63 deletions(-) create mode 100644 .dockerignore create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.dev.yaml create mode 100644 src/core/utils/type-check.ts delete mode 100644 src/routes/container-logs.ts create mode 100644 src/routes/docker-manager.ts create mode 100644 src/routes/docker-stats.ts delete mode 100644 src/routes/docker.ts create mode 100644 src/typings/docker.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..f965aed1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +node_modules +Dockerfile* +docker-compose* +.dockerignore +.git +.gitignore +README.md +LICENSE +.vscode +Makefile +helm-charts +.env +.editorconfig +.idea +coverage* diff --git a/bun.lock b/bun.lock index a5ee6c82..cf94952a 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,9 @@ "name": "dockstatapi", "dependencies": { "@elysiajs/swagger": "^1.2.2", + "@types/dockerode": "^3.3.34", "chalk": "^5.4.1", + "dockerode": "^4.0.4", "elysia": "latest", "winston": "^3.17.0", "winston-transport": "^4.9.0", @@ -15,13 +17,44 @@ }, }, }, + "trustedDependencies": [ + "protobufjs", + ], "packages": { + "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.12.6", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], + + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], "@scalar/themes": ["@scalar/themes@0.9.68", "", { "dependencies": { "@scalar/types": "0.0.34" } }, "sha512-466ac2fdQJOBBSLkGUf88vuZVF+qNMeVpjb0aAHrKkxhpjucTPKdTYO8r2dsX1R5k9A13gWPnm594VW5G/bGHw=="], @@ -30,20 +63,46 @@ "@sinclair/typebox": ["@sinclair/typebox@0.34.27", "", {}, "sha512-C7mxE1VC3WC2McOufZXEU48IfRVI+BcKxk4NOyNn3+JMUNdJHEWGS5CqjuDX+ij2NCCz8/nse1mT7yn8Fv2GHg=="], + "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], + + "@types/dockerode": ["@types/dockerode@3.3.34", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-mH9SuIb8NuTDsMus5epcbTzSbEo52fKLBMo0zapzYIAIyfDqoIFn7L3trekHLKC8qmxGV++pPUP4YqQ9n5v2Zg=="], + "@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "@types/ssh2": ["@types/ssh2@1.15.4", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-9JTQgVBWSgq6mAen6PVnrAmty1lqgCMvpfN+1Ck5WRUsyMYPa6qd50/vMJ0y1zkGpOEgLzm8m8Dx/Y5vRouLaA=="], + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], "@unhead/schema": ["@unhead/schema@1.11.19", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], + "bun-types": ["bun-types@1.2.3", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P7AeyTseLKAvgaZqQrvp3RqFM3yN9PlcLuSTe7SoJOfZkER73mLdT2vEQi8U64S1YvM/ldcNiQjn0Sn7H9lGgg=="], "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], @@ -56,64 +115,138 @@ "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], + + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], + + "dockerode": ["dockerode@4.0.4", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.0.1", "uuid": "^10.0.0" } }, "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w=="], + "elysia": ["elysia@1.2.21", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-E9b1JcB7fiQ2ptk24W8OnBrMYUoKzffIXob9uTVUKhqOKxaXAd9UyWBeyr7JCDa/VD/b/9S8aIey9/YJsK5sLg=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + "long": ["long@5.3.1", "", {}, "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng=="], + "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nan": ["nan@2.22.1", "", {}, "sha512-pfRR4ZcNTSm2ZFHaztuvbICf+hyiG6ecA06SfAxoPmuHjvMu0KUIae7Y8GyVkbBqeEIidsmXeYooWIX9+qjfRQ=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], + + "ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="], + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "tar-fs": ["tar-fs@2.0.1", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.0.0" } }, "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], "@scalar/themes/@scalar/types": ["@scalar/types@0.0.34", "", { "dependencies": { "@scalar/openapi-types": "0.1.8", "@unhead/schema": "^1.11.11" } }, "sha512-q01ctijmHArM5KOny2zU+sHfhpsgOAENrDENecK2TsQNn5FYLmFZouMKeW2M6F7KFLPZnFxUiL/rT88b6Rp/Kg=="], + "@types/ssh2/@types/node": ["@types/node@18.19.76", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw=="], + + "ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.8", "", {}, "sha512-iufA5/6hPCmRIVD2eh7qGpoKvoA08Gw/qUb2JECifBtAwA93fo7+1k9uHK440f2LMJsbxIzA+nv7RS0BmfiO/g=="], + + "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], } } diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..fdd42344 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,3 @@ +FROM oven/bun AS base + +WORKDIR /base diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml new file mode 100644 index 00000000..39da6d6b --- /dev/null +++ b/docker/docker-compose.dev.yaml @@ -0,0 +1,52 @@ +name: "DockStatAPI - Dev" +services: + socket-proxy: + container_name: Socket-Proxy + image: lscr.io/linuxserver/socket-proxy:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + restart: unless-stopped + read_only: true + tmpfs: + - /run + ports: + - 2375:2375 + environment: + - ALLOW_START=1 #optional + - ALLOW_STOP=1 #optional + - ALLOW_RESTARTS=1 #optional + - AUTH=1 #optional + - BUILD=1 #optional + - COMMIT=1 #optional + - CONFIGS=1 #optional + - CONTAINERS=1 #optional + - DISABLE_IPV6=1 #optional + - DISTRIBUTION=1 #optional + - EVENTS=1 #optional + - EXEC=1 #optional + - IMAGES=1 #optional + - INFO=1 #optional + - NETWORKS=1 #optional + - NODES=1 #optional + - PING=1 #optional + - PLUGINS=1 #optional + - POST=1 #optional + - PROXY_READ_TIMEOUT=240 #optional + - SECRETS=1 #optional + - SERVICES=1 #optional + - SESSION=1 #optional + - SWARM=1 #optional + - SYSTEM=1 #optional + - TASKS=1 #optional + - VERSION=1 #optional + - VOLUMES=1 #optional + + sqlite-web: + container_name: SQLite-web + image: ghcr.io/coleifer/sqlite-web:latest + ports: + - 8080:8080 + volumes: + - ../:/data:ro + environment: + - SQLITE_DATABASE=dockstatapi.db diff --git a/package.json b/package.json index 0e1deb8b..d7548382 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,13 @@ "version": "2.1.0", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "bun run --watch src/index.ts" + "dev": "docker compose -f ./docker/docker-compose.dev.yaml up -d && bun run --watch src/index.ts" }, "dependencies": { "@elysiajs/swagger": "^1.2.2", + "@types/dockerode": "^3.3.34", "chalk": "^5.4.1", + "dockerode": "^4.0.4", "elysia": "latest", "winston": "^3.17.0", "winston-transport": "^4.9.0" @@ -15,5 +17,8 @@ "devDependencies": { "bun-types": "latest" }, - "module": "src/index.js" + "module": "src/index.js", + "trustedDependencies": [ + "protobufjs" + ] } diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 6d5ea554..6ef778bd 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -1,4 +1,6 @@ import Database from "bun:sqlite"; +import { logger } from "~/core/utils/logger"; +import { typeCheck } from "~/core/utils/type-check"; const db = new Database("dockstatapi.db"); @@ -6,7 +8,6 @@ export const dbFunctions = { init() { db.exec(` CREATE TABLE IF NOT EXISTS docker_hosts ( - id TEXT PRIMARY KEY, name TEXT, url TEXT, poll_interval INTEGER @@ -30,11 +31,51 @@ export const dbFunctions = { `); }, - insertMetric(hostId: string, metric: any) { + addDockerHost(hostId: string, url: string, pollInterval: number) { + if ( + !typeCheck(hostId, "string") || + !typeCheck(url, "string") || + !typeCheck(pollInterval, "number") + ) { + logger.crit("Invalid parameter types for addDockerHost"); + throw new TypeError("Invalid parameter types for addDockerHost"); + } + + const stmt = db.prepare(` + INSERT INTO docker_hosts (name, url, poll_interval) + VALUES (?, ?, ?) + `); + return stmt.run(hostId, url, pollInterval); + }, + + getDockerHosts() { const stmt = db.prepare(` - INSERT INTO container_metrics (host_id, container_id, cpu, memory) - VALUES (?, ?, ?, ?) + SELECT name, url, poll_interval + FROM docker_hosts + ORDER BY name DESC `); + return stmt.all(); + }, + + insertMetric(hostId: string, metric: any) { + if (!typeCheck(hostId, "string") || !typeCheck(metric, "object")) { + logger.crit("Invalid parameter types for insertMetric"); + throw new TypeError("Invalid parameter types for insertMetric"); + } + + if ( + !typeCheck(metric.containerId, "string") || + !typeCheck(metric.cpu, "number") || + !typeCheck(metric.memory, "number") + ) { + logger.crit("Invalid metric object structure"); + throw new TypeError("Invalid metric object structure"); + } + + const stmt = db.prepare(` + INSERT INTO container_metrics (host_id, container_id, cpu, memory) + VALUES (?, ?, ?, ?) + `); return stmt.run(hostId, metric.containerId, metric.cpu, metric.memory); }, @@ -44,30 +85,96 @@ export const dbFunctions = { file_name: string, line: number, ) => { + if ( + !typeCheck(level, "string") || + !typeCheck(message, "string") || + !typeCheck(file_name, "string") || + !typeCheck(line, "number") + ) { + logger.crit("Invalid parameter types for addLogEntry"); + throw new TypeError("Invalid parameter types for addLogEntry"); + } + const stmt = db.prepare(` - INSERT INTO backend_log_entries (level, message, file, line) - VALUES (?, ?, ?, ?) - `); + INSERT INTO backend_log_entries (level, message, file, line) + VALUES (?, ?, ?, ?) + `); return stmt.run(level, message, file_name, line); }, getAllLogs() { const stmt = db.prepare(` - SELECT timestamp, level, message, file, line - FROM backend_log_entries - ORDER BY timestamp DESC - `); + SELECT timestamp, level, message, file, line + FROM backend_log_entries + ORDER BY timestamp DESC + `); return stmt.all(); }, getLogsByLevel(level: string) { + if (!typeCheck(level, "string")) { + logger.crit("Level parameter must be a string"); + throw new TypeError("Level parameter must be a string"); + } + + const stmt = db.prepare(` + SELECT timestamp, level, message, file, line + FROM backend_log_entries + WHERE level = ? + ORDER BY timestamp DESC + `); + return stmt.all(level); + }, + + updateDockerHost(name: string, url: string, pollInterval: number) { + if ( + !typeCheck(name, "string") || + !typeCheck(url, "string") || + !typeCheck(pollInterval, "number") + ) { + logger.crit("Invalid parameter types for updateDockerHost"); + throw new TypeError("Invalid parameter types for updateDockerHost"); + } + + const stmt = db.prepare(` + UPDATE docker_hosts + SET url = ?, poll_interval = ? + WHERE name = ? + `); + return stmt.run(url, pollInterval, name); + }, + + deleteDockerHost(name: string) { + if (!typeCheck(name, "string")) { + logger.crit("Invalid parameter type for deleteDockerHost"); + throw new TypeError("Name parameter must be a string"); + } + const stmt = db.prepare(` - SELECT timestamp, level, message, file, line - FROM backend_log_entries + DELETE FROM docker_hosts + WHERE name = ? + `); + return stmt.run(name); + }, + + clearAllLogs() { + const stmt = db.prepare(` + DELETE FROM backend_log_entries + `); + return stmt.run(); + }, + + clearLogsByLevel(level: string) { + if (!typeCheck(level, "string")) { + logger.crit("Invalid parameter type for clearLogsByLevel"); + throw new TypeError("Level parameter must be a string"); + } + + const stmt = db.prepare(` + DELETE FROM backend_log_entries WHERE level = ? - ORDER BY timestamp DESC `); - return stmt.all(level); + return stmt.run(level); }, }; diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 076e3857..1675d230 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -2,7 +2,7 @@ import { createLogger, format, transports } from "winston"; import Transport from "winston-transport"; import path from "path"; import { dbFunctions } from "../database/repository"; -import chalk from "chalk"; +import chalk, { ChalkInstance } from "chalk"; const fileLineFormat = format((info) => { try { @@ -48,7 +48,7 @@ export const logger = createLogger({ new transports.Console({ format: format.combine( format.printf(({ level, message, file, line }) => { - const levelColors: { [key: string]: chalk.Chalk } = { + const levelColors: { [key: string]: ChalkInstance } = { error: chalk.red.bold, warn: chalk.yellow.bold, info: chalk.green.bold, @@ -57,13 +57,13 @@ export const logger = createLogger({ silly: chalk.magenta.bold, }; - const paddedLevel = level.padEnd(5).toUpperCase(); + const paddedLevel = level.toUpperCase(); const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); const coloredContext = chalk.cyan(`${file}:${line}`); const coloredMessage = chalk.gray(message); - return `[ ${coloredContext.padEnd(22)} ] ${coloredLevel} - ${coloredMessage}`; + return `${coloredLevel} [ ${coloredContext} ] - ${coloredMessage}`; }), ), }), diff --git a/src/core/utils/type-check.ts b/src/core/utils/type-check.ts new file mode 100644 index 00000000..8675f79e --- /dev/null +++ b/src/core/utils/type-check.ts @@ -0,0 +1,28 @@ +type TypeCheck = [any, string]; + +export function typeCheck(value: any, expectedType: string): boolean { + if (expectedType === "null") { + return value === null; + } + + if (expectedType === "array") { + return Array.isArray(value); + } + + const actualType = typeof value; + + if (actualType === "object" && value !== null) { + if (expectedType === "object") { + return !Array.isArray(value); + } + return false; + } + + return actualType === expectedType; +} + +export function validateTypes(checks: TypeCheck[]): boolean[] { + return checks.map(([value, expectedType]) => { + return typeCheck(value, expectedType.toLowerCase()); + }); +} diff --git a/src/index.ts b/src/index.ts index dcca5a9a..576d42a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ -import { Elysia } from "elysia"; import { swagger } from "@elysiajs/swagger"; -import { loadPlugins } from "~/core/plugins/loader"; -import { dockerRoutes } from "~/routes/docker"; -import { logRoutes } from "~/routes/container-logs"; -import { backendLogs } from "./routes/logs"; +import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database/repository"; +import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; +import { dockerRoutes } from "~/routes/docker-manager"; +import { dockerStatsRoutes } from "~/routes/docker-stats"; +import { backendLogs } from "./routes/logs"; dbFunctions.init(); @@ -15,14 +15,14 @@ const app = new Elysia() documentation: { info: { title: "DockStatAPI", - version: "0.1.0", + version: "2.1.0", description: "Docker monitoring API with plugin support", }, }, }), ) .use(dockerRoutes) - .use(logRoutes) + .use(dockerStatsRoutes) .use(backendLogs) .get("/health", () => ({ status: "healthy" })); @@ -31,9 +31,9 @@ async function startServer() { await loadPlugins("./plugins"); app.listen(3000, ({ hostname, port }) => { - logger.info(`🦊 Elysia is running at http://${hostname}:${port}`); + logger.info(`DockStat is running at http://${hostname}:${port}`); logger.info( - `📚 API Documentation available at http://${hostname}:${port}/swagger`, + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, ); }); } catch (error) { diff --git a/src/routes/container-logs.ts b/src/routes/container-logs.ts deleted file mode 100644 index 085b19e4..00000000 --- a/src/routes/container-logs.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Elysia } from "elysia"; - -export const logRoutes = new Elysia({ prefix: "/logs" }).ws("/:containerId", { - open(ws) { - const containerId = ws.data.params.containerId; - console.log(`New log connection for ${containerId}`); - }, - message(ws, message) { - ws.send(message); - }, -}); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts new file mode 100644 index 00000000..ed2e5ff9 --- /dev/null +++ b/src/routes/docker-manager.ts @@ -0,0 +1,42 @@ +import { Elysia, t } from "elysia"; +import { dockerHostManager } from "~/core/docker/host-manager"; +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; + +export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) + .post( + "/add-host", + async ({ set, body }) => { + try { + const { id, url, pollInterval } = body; + set.headers["Content-Type"] = "application/json"; + dbFunctions.addDockerHost(id, url, pollInterval); + logger.debug(`Added docker host (${id})`); + return { success: true }; + } catch (error) { + set.status = 500; + logger.error("Failed to add host,", error); + return { error: "Failed to add host" }; + } + }, + { + body: t.Object({ + id: t.String(), + url: t.String(), + pollInterval: t.Number(), + }), + }, + ) + + .get("/hosts", async ({ set }) => { + try { + const dockerHosts = dbFunctions.getDockerHosts(); + set.headers["Content-Type"] = "application/json"; + logger.debug("Retrieved docker hosts"); + return dockerHosts; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve hosts,", error); + return { error: "Failed to retrieve hosts" }; + } + }); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts new file mode 100644 index 00000000..5bb9da20 --- /dev/null +++ b/src/routes/docker-stats.ts @@ -0,0 +1,243 @@ +import { Elysia, t } from "elysia"; +import Docker from "dockerode"; +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; +import type { HostConfig, DockerHost, ContainerInfo } from "~/typings/docker"; + +interface WsData { + params: any; + interval?: ReturnType; + statsStream?: any; +} + +const getDockerClient = (hostUrl: string): Docker => { + try { + const [host, port] = hostUrl.includes("://") + ? hostUrl.split("://")[1].split(":") + : hostUrl.split(":"); + + const protocol = hostUrl.startsWith("https://") ? "https" : "http"; + + return new Docker({ + protocol, + host, + port: port ? parseInt(port) : protocol === "https" ? 2376 : 2375, + version: "v1.41", + // TODO: Add TLS configuration if needed + }); + } catch (error) { + logger.error("Invalid Docker host URL configuration,", error); + throw new Error("Invalid Docker host configuration"); + } +}; + +export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) + .get("/containers", async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; + + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host.url); + try { + await docker.ping(); + } catch (pingError) { + logger.error("Docker host connection failed,", pingError); + return; + } + + const hostContainers = await docker.listContainers({ all: true }); + + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (err, stats) => { + if (err) { + logger.error("An error occured,", err); + return reject(err); + } + if (!stats) { + logger.error("No stats available"); + return reject(new Error("No stats available")); + } + resolve(stats); + }); + }, + ); + + containers.push({ + id: containerInfo.Id, + hostId: host.name, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (hostError) { + logger.error("Error fetching containers for host,", hostError); + } + }), + ); + + set.headers["Content-Type"] = "application/json"; + return { containers }; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve containers,", error); + return { error: "Failed to retrieve containers" }; + } + }) + + .get("/hosts/:id/config", async ({ params, set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const host = hosts.find((h) => h.name === params.id); + + if (!host) { + set.status = 404; + logger.error(`Host (${host}) not found`); + return { error: "Host not found" }; + } + + const docker = getDockerClient(host.url); + const info = await docker.info(); + + const config: HostConfig = { + hostId: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + }; + + set.headers["Content-Type"] = "application/json"; + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve host config,", error); + return { error: "Failed to retrieve host config" }; + } + }) + + .ws("/hosts/:id/stats", { + message(ws, message) { + ws.send(message); + }, + async open(ws) { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const host = hosts.find((h) => h.name === ws.data.params.id); + + if (!host) { + ws.close(1008, "Host not found"); + logger.error(`Host (${host}) not found`); + return; + } + + const docker = getDockerClient(host.url); + const interval = setInterval(async () => { + try { + const info = await docker.info(); + ws.send({ + timestamp: Date.now(), + memoryUsage: info.MemTotal - info.MemFree, + cpuUsage: info.NanoCPUs, + containerCount: info.ContainersRunning, + }); + logger.debug(`Fetched host (${host.name}) config`); + } catch (error) { + logger.error("Error fetching host stats,", error); + } + }, 5000); + (ws.data as WsData).interval = interval; + } catch (error) { + logger.error("WebSocket connection failed,", error); + ws.close(1011, "Internal error"); + } + }, + close(ws) { + const data = ws.data as WsData; + if (data.interval) { + clearInterval(data.interval); + } + }, + }) + + .ws("/containers/:hostId/:containerId/metrics", { + message(ws, message) { + ws.send(message); + }, + async open(ws) { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const host = hosts.find((h) => h.name === ws.data.params.hostId); + + if (!host) { + ws.close(1008, "Host not found"); + logger.error(`Host (${host}) not found`); + return; + } + + const docker = getDockerClient(host.url); + const container = docker.getContainer(ws.data.params.containerId); + const statsStream = await container.stats({ stream: true }); + + statsStream.on("data", (data: Buffer) => { + const stats = JSON.parse(data.toString()); + ws.send({ + cpu: calculateCpuPercent(stats), + memory: calculateMemoryUsage(stats), + timestamp: Date.now(), + }); + }); + + statsStream.on("error", (error) => { + logger.error("Container stats stream error,", error); + ws.close(1011, "Stats stream error"); + }); + + (ws.data as WsData).statsStream = statsStream; + } catch (error) { + logger.error("WebSocket connection failed,", error); + ws.close(1011, "Internal error"); + } + }, + close(ws) { + const data = ws.data as WsData; + if (data.statsStream) { + data.statsStream.destroy(); + } + }, + }); + +const calculateCpuPercent = (stats: Docker.ContainerStats): number => { + const cpuDelta = + stats.cpu_stats.cpu_usage.total_usage - + stats.precpu_stats.cpu_usage.total_usage; + const systemDelta = + stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + return (cpuDelta / systemDelta) * 100; +}; + +const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { + return (stats.memory_stats.usage / stats.memory_stats.limit) * 100; +}; diff --git a/src/routes/docker.ts b/src/routes/docker.ts deleted file mode 100644 index 993ae386..00000000 --- a/src/routes/docker.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Elysia, t } from "elysia"; -import { dockerHostManager } from "../core/docker/host-manager"; - -export const dockerRoutes = new Elysia({ prefix: "/docker-hosts" }) - .post( - "/", - async ({ body }) => { - const { id, url } = body; - await dockerHostManager.connect(id, url); - return { success: true }; - }, - { - body: t.Object({ - id: t.String(), - url: t.String(), - pollInterval: t.Number(), - }), - }, - ) - .get("/", () => { - return Array.from(dockerHostManager.connections.keys()); - }); diff --git a/src/routes/logs.ts b/src/routes/logs.ts index c4160001..5501f09d 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -27,4 +27,30 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) logger.error("Failed to retrieve logs"); return { error: "Failed to retrieve logs" }; } + }) + + .delete("/", async ({ set }) => { + try { + set.status = 200; + set.headers["Content-Type"] = "application/json"; + dbFunctions.clearAllLogs(); + return { success: true }; + } catch (error) { + set.status = 500; + logger.error("Could not delete all logs,", error); + return { error: "Could not delete all logs" }; + } + }) + + .delete("/:level", async ({ params: { level }, set }) => { + try { + dbFunctions.clearLogsByLevel(level); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Cleared all logs with level: ${level}`); + return { success: true }; + } catch (error) { + set.status = 500; + logger.error("Could not clear logs with level", level, ",", error); + return { error: "Failed to retrieve logs" }; + } }); diff --git a/src/typings/docker.ts b/src/typings/docker.ts new file mode 100644 index 00000000..e5294bb8 --- /dev/null +++ b/src/typings/docker.ts @@ -0,0 +1,28 @@ +interface DockerHost { + name: string; + url: string; + poll_interval: number; +} + +interface ContainerInfo { + id: string; + hostId: string; + name: string; + image: string; + status: string; + state: string; + cpuUsage: number; + memoryUsage: number; +} + +interface HostConfig { + hostId: string; + dockerVersion: string; + apiVersion: string; + os: string; + architecture: string; + totalMemory: number; + totalCPU: number; +} + +export type { HostConfig, ContainerInfo, DockerHost }; From 32ffec8b1293531eca60053b03937527ece95794 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 25 Feb 2025 21:40:54 +0100 Subject: [PATCH 136/369] ToFix: Sockets are not closing after disconnecting --- bun.lock | 6 + package.json | 2 + src/core/database/repository.ts | 93 ++++---- src/core/docker/client.ts | 20 ++ src/core/docker/host-manager.ts | 38 ---- src/core/utils/calculations.ts | 16 ++ src/core/utils/logger.ts | 2 +- src/core/utils/respone-handler.ts | 46 ++++ src/index.ts | 25 ++- src/routes/api-config.ts | 52 +++++ src/routes/docker-manager.ts | 84 ++++++-- src/routes/docker-stats.ts | 345 +++++++++++------------------- src/routes/docker-websocket.ts | 201 +++++++++++++++++ src/routes/logs.ts | 128 ++++++----- src/typings/database.ts | 13 ++ src/typings/docker.ts | 2 +- 16 files changed, 701 insertions(+), 372 deletions(-) create mode 100644 src/core/docker/client.ts delete mode 100644 src/core/docker/host-manager.ts create mode 100644 src/core/utils/calculations.ts create mode 100644 src/core/utils/respone-handler.ts create mode 100644 src/routes/api-config.ts create mode 100644 src/routes/docker-websocket.ts create mode 100644 src/typings/database.ts diff --git a/bun.lock b/bun.lock index cf94952a..2c9571a8 100644 --- a/bun.lock +++ b/bun.lock @@ -6,9 +6,11 @@ "dependencies": { "@elysiajs/swagger": "^1.2.2", "@types/dockerode": "^3.3.34", + "@types/split2": "^4.2.3", "chalk": "^5.4.1", "dockerode": "^4.0.4", "elysia": "latest", + "split2": "^4.2.0", "winston": "^3.17.0", "winston-transport": "^4.9.0", }, @@ -69,6 +71,8 @@ "@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], + "@types/ssh2": ["@types/ssh2@1.15.4", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-9JTQgVBWSgq6mAen6PVnrAmty1lqgCMvpfN+1Ck5WRUsyMYPa6qd50/vMJ0y1zkGpOEgLzm8m8Dx/Y5vRouLaA=="], "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], @@ -195,6 +199,8 @@ "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="], "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], diff --git a/package.json b/package.json index d7548382..ff72d906 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,11 @@ "dependencies": { "@elysiajs/swagger": "^1.2.2", "@types/dockerode": "^3.3.34", + "@types/split2": "^4.2.3", "chalk": "^5.4.1", "dockerode": "^4.0.4", "elysia": "latest", + "split2": "^4.2.0", "winston": "^3.17.0", "winston-transport": "^4.9.0" }, diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 6ef778bd..6c0a62b9 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -1,6 +1,8 @@ import Database from "bun:sqlite"; import { logger } from "~/core/utils/logger"; import { typeCheck } from "~/core/utils/type-check"; +import { config } from "~/typings/database"; +import type { DockerHost } from "~/typings/docker"; const db = new Database("dockstatapi.db"); @@ -10,15 +12,11 @@ export const dbFunctions = { CREATE TABLE IF NOT EXISTS docker_hosts ( name TEXT, url TEXT, - poll_interval INTEGER + secure BOOLEAN ); - CREATE TABLE IF NOT EXISTS container_metrics ( - host_id TEXT, - container_id TEXT, - cpu REAL, - memory REAL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + CREATE TABLE IF NOT EXISTS config ( + polling_rate NUMBER ); CREATE TABLE IF NOT EXISTS backend_log_entries ( @@ -29,54 +27,46 @@ export const dbFunctions = { line NUMBER ); `); + + const configRow = db + .prepare(`SELECT COUNT(*) AS count FROM config`) + .get() as { count: number }; + if (configRow.count === 0) { + const stmt = db.prepare( + ` + INSERT INTO config (polling_rate) VALUES (5) + `, + ); + + stmt.run(); + } }, - addDockerHost(hostId: string, url: string, pollInterval: number) { + addDockerHost(hostId: string, url: string, secure: boolean) { if ( !typeCheck(hostId, "string") || !typeCheck(url, "string") || - !typeCheck(pollInterval, "number") + !typeCheck(secure, "boolean") ) { logger.crit("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); } const stmt = db.prepare(` - INSERT INTO docker_hosts (name, url, poll_interval) + INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) `); - return stmt.run(hostId, url, pollInterval); + return stmt.run(hostId, url, secure); }, - getDockerHosts() { + getDockerHosts(): DockerHost[] { const stmt = db.prepare(` - SELECT name, url, poll_interval + SELECT name, url, secure FROM docker_hosts ORDER BY name DESC `); - return stmt.all(); - }, - - insertMetric(hostId: string, metric: any) { - if (!typeCheck(hostId, "string") || !typeCheck(metric, "object")) { - logger.crit("Invalid parameter types for insertMetric"); - throw new TypeError("Invalid parameter types for insertMetric"); - } - - if ( - !typeCheck(metric.containerId, "string") || - !typeCheck(metric.cpu, "number") || - !typeCheck(metric.memory, "number") - ) { - logger.crit("Invalid metric object structure"); - throw new TypeError("Invalid metric object structure"); - } - - const stmt = db.prepare(` - INSERT INTO container_metrics (host_id, container_id, cpu, memory) - VALUES (?, ?, ?, ?) - `); - return stmt.run(hostId, metric.containerId, metric.cpu, metric.memory); + const data = stmt.all(); + return data as DockerHost[]; }, addLogEntry: ( @@ -126,11 +116,11 @@ export const dbFunctions = { return stmt.all(level); }, - updateDockerHost(name: string, url: string, pollInterval: number) { + updateDockerHost(name: string, url: string, secure: boolean) { if ( !typeCheck(name, "string") || !typeCheck(url, "string") || - !typeCheck(pollInterval, "number") + !typeCheck(secure, "boolean") ) { logger.crit("Invalid parameter types for updateDockerHost"); throw new TypeError("Invalid parameter types for updateDockerHost"); @@ -138,10 +128,10 @@ export const dbFunctions = { const stmt = db.prepare(` UPDATE docker_hosts - SET url = ?, poll_interval = ? + SET url = ?, secure = ? WHERE name = ? `); - return stmt.run(url, pollInterval, name); + return stmt.run(url, secure, name); }, deleteDockerHost(name: string) { @@ -176,6 +166,29 @@ export const dbFunctions = { `); return stmt.run(level); }, + + updateConfig(polling_rate: number) { + if (!typeCheck(polling_rate, "number")) { + logger.crit("Invalid parameter type for updateConfig"); + throw new TypeError("Polling rate must be a number!"); + } + + const stmt = db.prepare(` + UPDATE config + SET polling_rate = ? + `); + + return stmt.run(polling_rate); + }, + + getConfig() { + const stmt = db.prepare(` + SELECT distinct(polling_rate) + FROM config + `); + + return stmt.all(); + }, }; dbFunctions.init(); diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts new file mode 100644 index 00000000..da174032 --- /dev/null +++ b/src/core/docker/client.ts @@ -0,0 +1,20 @@ +import type { DockerHost } from "~/typings/docker"; +import Docker from "dockerode"; +import { logger } from "~/core/utils/logger"; + +export const getDockerClient = (host: DockerHost): Docker => { + try { + const [hostAddress, port] = host.url.split(":"); + const protocol = host.secure ? "https" : "http"; + return new Docker({ + protocol, + host: hostAddress, + port: port ? parseInt(port) : host.secure ? 2376 : 2375, + version: "v1.41", + // TODO: Add TLS configuration if needed + }); + } catch (error) { + logger.error("Invalid Docker host URL configuration,", error); + throw new Error("Invalid Docker host configuration"); + } +}; diff --git a/src/core/docker/host-manager.ts b/src/core/docker/host-manager.ts deleted file mode 100644 index e2c1ccc4..00000000 --- a/src/core/docker/host-manager.ts +++ /dev/null @@ -1,38 +0,0 @@ -import WebSocket from "ws"; -import { pluginManager } from "~/core/plugins/plugin-manager"; -import { dbFunctions } from "~/core/database/repository"; -import { logger } from "~/core/utils/logger"; - -export class DockerHostManager { - public connections = new Map(); - - async connect(hostId: string, url: string) { - const ws = new WebSocket(url); - - ws.on("open", () => { - this.connections.set(hostId, ws); - logger.info(`Opened connection to ${hostId}`); - }); - - ws.on("message", (data) => { - this.handleData(hostId, JSON.parse(data.toString())); - }); - - ws.on("close", () => { - this.connections.delete(hostId); - logger.info(`Disconnected from Docker host ${hostId}`); - }); - } - - private handleData(hostId: string, data: any) { - dbFunctions.insertMetric(hostId, data); - - if (data.event === "container_start") { - pluginManager.handleContainerStart(data.container); - } - - pluginManager.handleMetrics(data); - } -} - -export const dockerHostManager = new DockerHostManager(); diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts new file mode 100644 index 00000000..3ead3a6d --- /dev/null +++ b/src/core/utils/calculations.ts @@ -0,0 +1,16 @@ +import type Docker from "dockerode"; + +const calculateCpuPercent = (stats: Docker.ContainerStats): number => { + const cpuDelta = + stats.cpu_stats.cpu_usage.total_usage - + stats.precpu_stats.cpu_usage.total_usage; + const systemDelta = + stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + return (cpuDelta / systemDelta) * 100; +}; + +const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { + return (stats.memory_stats.usage / stats.memory_stats.limit) * 100; +}; + +export { calculateCpuPercent, calculateMemoryUsage }; diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 1675d230..c704d0a0 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -1,7 +1,7 @@ import { createLogger, format, transports } from "winston"; import Transport from "winston-transport"; import path from "path"; -import { dbFunctions } from "../database/repository"; +import { dbFunctions } from "~/core/database/repository"; import chalk, { ChalkInstance } from "chalk"; const fileLineFormat = format((info) => { diff --git a/src/core/utils/respone-handler.ts b/src/core/utils/respone-handler.ts new file mode 100644 index 00000000..93e0cdbe --- /dev/null +++ b/src/core/utils/respone-handler.ts @@ -0,0 +1,46 @@ +import { logger } from "~/core/utils/logger"; +import type { HTTPHeaders } from "elysia/dist/types"; +import type { ElysiaCookie } from "elysia/dist/cookies"; +import type { StatusMap } from "elysia"; + +interface set { + headers: HTTPHeaders; + status?: number | keyof StatusMap; + redirect?: string; + cookie?: Record; +} + +export const responseHandler = { + error( + set: set, + error: string, + response_message: string, + error_code?: number, + ) { + set.status = error_code || 500; + logger.error(`${response_message} - ${error}`); + return { error: `${response_message}` }; + }, + + ok(set: set, response_message: string) { + set.status = 200; + logger.debug(response_message); + return { success: true }; + }, + + simple_error(set: set, response_massage: string, status_code?: number) { + set.status = status_code || 502; + logger.warn(response_massage); + return { error: response_massage }; + }, + + reject(set: set, reject: any, response_message: string, error?: string) { + set.status = 501; + if (error) { + logger.error(`${response_message} - ${error}`); + } else { + logger.error(response_message); + } + return reject(new Error(response_message)); + }, +}; diff --git a/src/index.ts b/src/index.ts index 576d42a1..06e500d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,9 @@ import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { dockerRoutes } from "~/routes/docker-manager"; import { dockerStatsRoutes } from "~/routes/docker-stats"; -import { backendLogs } from "./routes/logs"; +import { backendLogs } from "~/routes/logs"; +import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; +import { apiConfigRoutes } from "~/routes/api-config"; dbFunctions.init(); @@ -18,20 +20,37 @@ const app = new Elysia() version: "2.1.0", description: "Docker monitoring API with plugin support", }, + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], }, }), ) .use(dockerRoutes) .use(dockerStatsRoutes) .use(backendLogs) - .get("/health", () => ({ status: "healthy" })); + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }); async function startServer() { try { await loadPlugins("./plugins"); app.listen(3000, ({ hostname, port }) => { - logger.info(`DockStat is running at http://${hostname}:${port}`); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( `Swagger API Documentation available at http://${hostname}:${port}/swagger`, ); diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts new file mode 100644 index 00000000..41262c85 --- /dev/null +++ b/src/routes/api-config.ts @@ -0,0 +1,52 @@ +import { Elysia, t } from "elysia"; +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; +import { responseHandler } from "~/core/utils/respone-handler"; +import { config } from "~/typings/database"; + +export const apiConfigRoutes = new Elysia({ prefix: "/config" }) + .get( + "/get", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; + set.headers["Content-Type"] = "application/json"; + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + return responseHandler.error( + set, + "Error getting the DockStatAPI config", + error as string, + ); + } + }, + { + tags: ["Management"], + }, + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { polling_rate } = body; + set.headers["Content-Type"] = "application/json"; + dbFunctions.updateConfig(polling_rate); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + return responseHandler.error( + set, + "Error updating the DockStatAPI config", + error as string, + ); + } + }, + { + body: t.Object({ + polling_rate: t.Number(), + }), + tags: ["Management"], + }, + ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index ed2e5ff9..eb53fdb9 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -1,42 +1,82 @@ import { Elysia, t } from "elysia"; -import { dockerHostManager } from "~/core/docker/host-manager"; import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; +import { responseHandler } from "~/core/utils/respone-handler"; export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) .post( "/add-host", async ({ set, body }) => { try { - const { id, url, pollInterval } = body; + const { name, url, secure } = body; set.headers["Content-Type"] = "application/json"; - dbFunctions.addDockerHost(id, url, pollInterval); - logger.debug(`Added docker host (${id})`); - return { success: true }; + dbFunctions.addDockerHost(name, url, secure); + return responseHandler.ok(set, `Added docker host (${name})`); + } catch (error: unknown) { + return responseHandler.error( + set, + "Error adding docker Host", + error as string, + ); + } + }, + { + detail: { + tags: ["Management"], + }, + body: t.Object({ + name: t.String(), + url: t.String(), + secure: t.Boolean(), + }), + }, + ) + + .post( + "/update-host", + async ({ set, body }) => { + try { + const { name, url, secure } = body; + dbFunctions.updateDockerHost(name, url, secure); } catch (error) { - set.status = 500; - logger.error("Failed to add host,", error); - return { error: "Failed to add host" }; + return responseHandler.error( + set, + error as string, + "Failed to update host", + ); } }, { + detail: { + tags: ["Management"], + }, body: t.Object({ - id: t.String(), + name: t.String(), url: t.String(), - pollInterval: t.Number(), + secure: t.Boolean(), }), }, ) - .get("/hosts", async ({ set }) => { - try { - const dockerHosts = dbFunctions.getDockerHosts(); - set.headers["Content-Type"] = "application/json"; - logger.debug("Retrieved docker hosts"); - return dockerHosts; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve hosts,", error); - return { error: "Failed to retrieve hosts" }; - } - }); + .get( + "/hosts", + async ({ set }) => { + try { + const dockerHosts = dbFunctions.getDockerHosts(); + set.headers["Content-Type"] = "application/json"; + logger.debug("Retrieved docker hosts"); + return dockerHosts; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve hosts", + ); + } + }, + { + detail: { + tags: ["Management"], + }, + }, + ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 5bb9da20..95e4a8b1 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -1,243 +1,150 @@ -import { Elysia, t } from "elysia"; import Docker from "dockerode"; +import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database/repository"; +import { getDockerClient } from "~/core/docker/client"; +import { + calculateCpuPercent, + calculateMemoryUsage, +} from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; -import type { HostConfig, DockerHost, ContainerInfo } from "~/typings/docker"; - -interface WsData { - params: any; - interval?: ReturnType; - statsStream?: any; -} - -const getDockerClient = (hostUrl: string): Docker => { - try { - const [host, port] = hostUrl.includes("://") - ? hostUrl.split("://")[1].split(":") - : hostUrl.split(":"); - - const protocol = hostUrl.startsWith("https://") ? "https" : "http"; - - return new Docker({ - protocol, - host, - port: port ? parseInt(port) : protocol === "https" ? 2376 : 2375, - version: "v1.41", - // TODO: Add TLS configuration if needed - }); - } catch (error) { - logger.error("Invalid Docker host URL configuration,", error); - throw new Error("Invalid Docker host configuration"); - } -}; +import { responseHandler } from "~/core/utils/respone-handler"; +import type { ContainerInfo, DockerHost, HostConfig } from "~/typings/docker"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get("/containers", async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; - - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host.url); - try { - await docker.ping(); - } catch (pingError) { - logger.error("Docker host connection failed,", pingError); - return; - } - - const hostContainers = await docker.listContainers({ all: true }); - - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (err, stats) => { - if (err) { - logger.error("An error occured,", err); - return reject(err); - } - if (!stats) { - logger.error("No stats available"); - return reject(new Error("No stats available")); - } - resolve(stats); - }); - }, - ); - - containers.push({ - id: containerInfo.Id, - hostId: host.name, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError, - ); - } - }), - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (hostError) { - logger.error("Error fetching containers for host,", hostError); - } - }), - ); - - set.headers["Content-Type"] = "application/json"; - return { containers }; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve containers,", error); - return { error: "Failed to retrieve containers" }; - } - }) - - .get("/hosts/:id/config", async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = hosts.find((h) => h.name === params.id); - - if (!host) { - set.status = 404; - logger.error(`Host (${host}) not found`); - return { error: "Host not found" }; - } - - const docker = getDockerClient(host.url); - const info = await docker.info(); - - const config: HostConfig = { - hostId: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - }; - - set.headers["Content-Type"] = "application/json"; - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve host config,", error); - return { error: "Failed to retrieve host config" }; - } - }) - - .ws("/hosts/:id/stats", { - message(ws, message) { - ws.send(message); - }, - async open(ws) { + .get( + "/containers", + async ({ set }) => { try { const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = hosts.find((h) => h.name === ws.data.params.id); + const containers: ContainerInfo[] = []; - if (!host) { - ws.close(1008, "Host not found"); - logger.error(`Host (${host}) not found`); - return; - } + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + return responseHandler.error( + set, + pingError as string, + "Docker host connection failed", + ); + } + + const hostContainers = await docker.listContainers({ all: true }); + + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return responseHandler.reject( + set, + reject, + "An error occurred", + error, + ); + } + if (!stats) { + return responseHandler.reject( + set, + reject, + "No stats available", + ); + } + resolve(stats); + }); + }, + ); + + containers.push({ + id: containerInfo.Id, + hostId: host.name, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (hostError) { + logger.error("Error fetching containers for host,", hostError); + } + }), + ); - const docker = getDockerClient(host.url); - const interval = setInterval(async () => { - try { - const info = await docker.info(); - ws.send({ - timestamp: Date.now(), - memoryUsage: info.MemTotal - info.MemFree, - cpuUsage: info.NanoCPUs, - containerCount: info.ContainersRunning, - }); - logger.debug(`Fetched host (${host.name}) config`); - } catch (error) { - logger.error("Error fetching host stats,", error); - } - }, 5000); - (ws.data as WsData).interval = interval; + set.headers["Content-Type"] = "application/json"; + logger.debug("Fetched all containers across all hosts"); + return { containers }; } catch (error) { - logger.error("WebSocket connection failed,", error); - ws.close(1011, "Internal error"); + return responseHandler.error( + set, + error as string, + "Failed to retrieve containers", + ); } }, - close(ws) { - const data = ws.data as WsData; - if (data.interval) { - clearInterval(data.interval); - } + { + detail: { + tags: ["Statistics"], + }, }, - }) + ) - .ws("/containers/:hostId/:containerId/metrics", { - message(ws, message) { - ws.send(message); - }, - async open(ws) { + .get( + "/hosts/:id", + async ({ params, set }) => { try { const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = hosts.find((h) => h.name === ws.data.params.hostId); + const host = hosts.find((h) => h.name === params.id); if (!host) { - ws.close(1008, "Host not found"); - logger.error(`Host (${host}) not found`); - return; + return responseHandler.simple_error( + set, + `Host (${params.id}) not found`, + ); } - const docker = getDockerClient(host.url); - const container = docker.getContainer(ws.data.params.containerId); - const statsStream = await container.stats({ stream: true }); - - statsStream.on("data", (data: Buffer) => { - const stats = JSON.parse(data.toString()); - ws.send({ - cpu: calculateCpuPercent(stats), - memory: calculateMemoryUsage(stats), - timestamp: Date.now(), - }); - }); - - statsStream.on("error", (error) => { - logger.error("Container stats stream error,", error); - ws.close(1011, "Stats stream error"); - }); - - (ws.data as WsData).statsStream = statsStream; + const docker = getDockerClient(host); + const info = await docker.info(); + + const config: HostConfig = { + hostId: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + }; + + set.headers["Content-Type"] = "application/json"; + logger.debug(`Fetched config for ${host.name}`); + return config; } catch (error) { - logger.error("WebSocket connection failed,", error); - ws.close(1011, "Internal error"); + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); } }, - close(ws) { - const data = ws.data as WsData; - if (data.statsStream) { - data.statsStream.destroy(); - } + { + detail: { + tags: ["Statistics"], + }, }, - }); - -const calculateCpuPercent = (stats: Docker.ContainerStats): number => { - const cpuDelta = - stats.cpu_stats.cpu_usage.total_usage - - stats.precpu_stats.cpu_usage.total_usage; - const systemDelta = - stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; - return (cpuDelta / systemDelta) * 100; -}; - -const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { - return (stats.memory_stats.usage / stats.memory_stats.limit) * 100; -}; + ); diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts new file mode 100644 index 00000000..ef7b7205 --- /dev/null +++ b/src/routes/docker-websocket.ts @@ -0,0 +1,201 @@ +import type { StatusMap } from "elysia"; +import { Elysia } from "elysia"; +import type { HTTPHeaders } from "elysia/dist/types"; +import { dbFunctions } from "~/core/database/repository"; +import { getDockerClient } from "~/core/docker/client"; +import { + calculateCpuPercent, + calculateMemoryUsage, +} from "~/core/utils/calculations"; +import { logger } from "~/core/utils/logger"; +import { responseHandler } from "~/core/utils/respone-handler"; +import type { DockerHost } from "~/typings/docker"; +import split2 from "split2"; + +const set: { headers: HTTPHeaders; status?: number | keyof StatusMap } = { + headers: {}, +}; + +export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( + "/stats", + { + async open(socket) { + socket.send(JSON.stringify({ message: "Connection established" })); + let hosts: DockerHost[]; + + // Track if the WebSocket is open + (socket as any).isOpen = true; + (socket as any).streams = []; + + logger.debug(`Opened WebSocket (${socket.id})`); + + try { + hosts = dbFunctions.getDockerHosts(); + logger.debug( + `Retrieved ${hosts.length} docker host(s) from the database`, + ); + } catch (error: unknown) { + const errResponse = responseHandler.error( + set, + (error as Error).message, + "Failed to retrieve Docker hosts", + 500, + ); + logger.error( + `Error retrieving Docker hosts: ${(error as Error).message}`, + ); + socket.send(JSON.stringify(errResponse)); + return; + } + + for (const host of hosts) { + if (!(socket as any).isOpen) break; + + logger.debug(`Processing host: ${host.name}`); + + try { + const docker = getDockerClient(host); + await docker.ping(); + logger.debug(`Ping successful for host: ${host.name}`); + logger.debug(`Listing containers for host: ${host.name}`); + const containers = await docker.listContainers(); + logger.debug( + `Found ${containers.length} container(s) on host ${host.name}`, + ); + + for (const containerInfo of containers) { + // Check if WebSocket is still open before processing each container + if (!(socket as any).isOpen) break; + + logger.debug( + `Processing container ${containerInfo.Id} on host ${host.name}`, + ); + const container = docker.getContainer(containerInfo.Id); + try { + logger.debug( + `Starting stats stream for container ${containerInfo.Id} on host ${host.name}`, + ); + const statsStream = await container.stats({ stream: true }); + + // Immediately destroy stream if WebSocket closed while setting up + if (!(socket as any).isOpen) { + statsStream.pause(); + statsStream.unpipe(); + continue; + } + + // Save stream for cleanup on socket close + (socket as any).streams.push(statsStream); + + // Use split2 to process NDJSON lines + statsStream + .pipe(split2()) + .on("data", (line: string) => { + if (!line) return; + try { + const stats = JSON.parse(line); + const cpuUsage = calculateCpuPercent(stats); + const memoryUsage = calculateMemoryUsage(stats); + + const data = { + id: containerInfo.Id, + hostId: host.name, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage, + memoryUsage, + }; + socket.send(JSON.stringify(data)); + logger.debug(`Parsing data`); + } catch (parseErr: any) { + logger.error( + `Failed to parse stats for container ${containerInfo.Id} on host ${host.name}: ${parseErr.message}`, + ); + } + }) + .on("error", (err: Error) => { + logger.error( + `Stats stream error for container ${containerInfo.Id} on host ${host.name}: ${err.message}`, + ); + const errResponse = responseHandler.error( + set, + err.message, + `Stats stream error for container ${containerInfo.Id}`, + 500, + ); + socket.send( + JSON.stringify({ + hostId: host.name, + containerId: containerInfo.Id, + error: errResponse.error, + }), + ); + statsStream.removeAllListeners(); + }); + + statsStream.resume(); + } catch (streamErr: any) { + logger.error( + `Failed to start stats stream for container ${containerInfo.Id} on host ${host.name}: ${streamErr.message}`, + ); + const errResponse = responseHandler.error( + set, + streamErr.message, + `Failed to start stats stream for container ${containerInfo.Id}`, + 500, + ); + socket.send( + JSON.stringify({ + hostId: host.name, + containerId: containerInfo.Id, + error: errResponse.error, + }), + ); + } + } + } catch (err: any) { + logger.error( + `Failed to list containers for host ${host.name}: ${err.message}`, + ); + const errResponse = responseHandler.error( + set, + err.message, + `Failed to list containers for host ${host.name}`, + 500, + ); + socket.send( + JSON.stringify({ + hostId: host.name, + error: errResponse.error, + }), + ); + } + } + }, + + close(socket, code, reason) { + //socket.isOpen = false; + + socket.close(1000); + //const streams = (socket as any).streams; + //if (streams?.length) { + // streams.forEach((stream: NodeJS.ReadableStream) => { + // try { + // logger.debug(`Destroying stats stream`); + // stream.pause(); + // stream.unpipe(); + // } catch (err) { + // logger.error(`Error destroying stream: ${err}`); + // } + // }); + // (socket as any).streams = []; + //} + + logger.info( + `Closed WebSocket (${socket.id}) - Code: ${code} - Reason: ${reason}`, + ); + }, + }, +); diff --git a/src/routes/logs.ts b/src/routes/logs.ts index 5501f09d..a8cae1c5 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -3,54 +3,86 @@ import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; export const backendLogs = new Elysia({ prefix: "/logs" }) - .get("/", async ({ set }) => { - try { - const logs = dbFunctions.getAllLogs(); - set.headers["Content-Type"] = "application/json"; - logger.debug(`Retrieved all logs`); - return logs; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve logs,", error); - return { error: "Failed to retrieve logs" }; - } - }) + .get( + "/", + async ({ set }) => { + try { + const logs = dbFunctions.getAllLogs(); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Retrieved all logs`); + return logs; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve logs,", error); + return { error: "Failed to retrieve logs" }; + } + }, + { + detail: { + tags: ["Management"], + }, + }, + ) - .get("/:level", async ({ params: { level }, set }) => { - try { - const logs = dbFunctions.getLogsByLevel(level); - set.headers["Content-Type"] = "application/json"; - logger.debug(`Retrieved logs (level: ${level})`); - return logs; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve logs"); - return { error: "Failed to retrieve logs" }; - } - }) + .get( + "/:level", + async ({ params: { level }, set }) => { + try { + const logs = dbFunctions.getLogsByLevel(level); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Retrieved logs (level: ${level})`); + return logs; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve logs"); + return { error: "Failed to retrieve logs" }; + } + }, + { + detail: { + tags: ["Management"], + }, + }, + ) - .delete("/", async ({ set }) => { - try { - set.status = 200; - set.headers["Content-Type"] = "application/json"; - dbFunctions.clearAllLogs(); - return { success: true }; - } catch (error) { - set.status = 500; - logger.error("Could not delete all logs,", error); - return { error: "Could not delete all logs" }; - } - }) + .delete( + "/", + async ({ set }) => { + try { + set.status = 200; + set.headers["Content-Type"] = "application/json"; + dbFunctions.clearAllLogs(); + return { success: true }; + } catch (error) { + set.status = 500; + logger.error("Could not delete all logs,", error); + return { error: "Could not delete all logs" }; + } + }, + { + detail: { + tags: ["Management"], + }, + }, + ) - .delete("/:level", async ({ params: { level }, set }) => { - try { - dbFunctions.clearLogsByLevel(level); - set.headers["Content-Type"] = "application/json"; - logger.debug(`Cleared all logs with level: ${level}`); - return { success: true }; - } catch (error) { - set.status = 500; - logger.error("Could not clear logs with level", level, ",", error); - return { error: "Failed to retrieve logs" }; - } - }); + .delete( + "/:level", + async ({ params: { level }, set }) => { + try { + dbFunctions.clearLogsByLevel(level); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Cleared all logs with level: ${level}`); + return { success: true }; + } catch (error) { + set.status = 500; + logger.error("Could not clear logs with level", level, ",", error); + return { error: "Failed to retrieve logs" }; + } + }, + { + detail: { + tags: ["Management"], + }, + }, + ); diff --git a/src/typings/database.ts b/src/typings/database.ts new file mode 100644 index 00000000..d39ccbf2 --- /dev/null +++ b/src/typings/database.ts @@ -0,0 +1,13 @@ +interface backend_log_entries { + timestamp: string; + level: string; + message: string; + file: string; + line: number; +} + +interface config { + polling_rate: number; +} + +export type { backend_log_entries, config }; diff --git a/src/typings/docker.ts b/src/typings/docker.ts index e5294bb8..8d78ae2b 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -1,7 +1,7 @@ interface DockerHost { name: string; url: string; - poll_interval: number; + secure: boolean; } interface ContainerInfo { From fef8744603a327b10419d5d92feb6800199cc19b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 25 Feb 2025 22:36:16 +0100 Subject: [PATCH 137/369] Fix: Webocket cleanup works? Let's go? --- src/routes/docker-websocket.ts | 148 ++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 56 deletions(-) diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index ef7b7205..09770dce 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -11,6 +11,7 @@ import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/respone-handler"; import type { DockerHost } from "~/typings/docker"; import split2 from "split2"; +import type { Readable } from "stream"; const set: { headers: HTTPHeaders; status?: number | keyof StatusMap } = { headers: {}, @@ -26,6 +27,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( // Track if the WebSocket is open (socket as any).isOpen = true; (socket as any).streams = []; + (socket as any).heartbeat = null; // Add heartbeat reference logger.debug(`Opened WebSocket (${socket.id})`); @@ -48,6 +50,15 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( return; } + // Add heartbeat using WebSocket protocol-level ping + (socket as any).heartbeat = setInterval(() => { + if (!(socket as any).isOpen) { + clearInterval((socket as any).heartbeat); + return; + } + socket.ping(); // Use WebSocket protocol ping + }, 30000); + for (const host of hosts) { if (!(socket as any).isOpen) break; @@ -64,7 +75,6 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( ); for (const containerInfo of containers) { - // Check if WebSocket is still open before processing each container if (!(socket as any).isOpen) break; logger.debug( @@ -75,22 +85,30 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( logger.debug( `Starting stats stream for container ${containerInfo.Id} on host ${host.name}`, ); - const statsStream = await container.stats({ stream: true }); + const statsStream = (await container.stats({ + stream: true, + })) as Readable; + const splitStream = split2(); - // Immediately destroy stream if WebSocket closed while setting up - if (!(socket as any).isOpen) { - statsStream.pause(); - statsStream.unpipe(); - continue; - } + // Store both streams for cleanup + (socket as any).streams.push({ statsStream, splitStream }); - // Save stream for cleanup on socket close - (socket as any).streams.push(statsStream); + // Handle stream lifecycle + statsStream + .on("close", () => { + logger.debug(`Stats stream closed for ${containerInfo.Id}`); + splitStream.destroy(); + }) + .on("end", () => { + logger.debug(`Stats stream ended for ${containerInfo.Id}`); + splitStream.destroy(); + }); - // Use split2 to process NDJSON lines + // Process data statsStream - .pipe(split2()) + .pipe(splitStream) .on("data", (line: string) => { + if (socket.readyState !== 1) return; // 1 = OPEN state if (!line) return; try { const stats = JSON.parse(line); @@ -108,7 +126,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( memoryUsage, }; socket.send(JSON.stringify(data)); - logger.debug(`Parsing data`); + logger.debug(`Parsing data on Socket ${socket.id}`); } catch (parseErr: any) { logger.error( `Failed to parse stats for container ${containerInfo.Id} on host ${host.name}: ${parseErr.message}`, @@ -125,17 +143,17 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( `Stats stream error for container ${containerInfo.Id}`, 500, ); - socket.send( - JSON.stringify({ - hostId: host.name, - containerId: containerInfo.Id, - error: errResponse.error, - }), - ); - statsStream.removeAllListeners(); + if (socket.readyState === 1) { + socket.send( + JSON.stringify({ + hostId: host.name, + containerId: containerInfo.Id, + error: errResponse.error, + }), + ); + } + statsStream.destroy(); }); - - statsStream.resume(); } catch (streamErr: any) { logger.error( `Failed to start stats stream for container ${containerInfo.Id} on host ${host.name}: ${streamErr.message}`, @@ -146,13 +164,15 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( `Failed to start stats stream for container ${containerInfo.Id}`, 500, ); - socket.send( - JSON.stringify({ - hostId: host.name, - containerId: containerInfo.Id, - error: errResponse.error, - }), - ); + if (socket.readyState === 1) { + socket.send( + JSON.stringify({ + hostId: host.name, + containerId: containerInfo.Id, + error: errResponse.error, + }), + ); + } } } } catch (err: any) { @@ -165,37 +185,53 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( `Failed to list containers for host ${host.name}`, 500, ); - socket.send( - JSON.stringify({ - hostId: host.name, - error: errResponse.error, - }), - ); + if (socket.readyState === 1) { + socket.send( + JSON.stringify({ + hostId: host.name, + error: errResponse.error, + }), + ); + } } } }, + message(socket, message) { + // Handle pong responses + if (message === "pong") return; + }, + close(socket, code, reason) { - //socket.isOpen = false; - - socket.close(1000); - //const streams = (socket as any).streams; - //if (streams?.length) { - // streams.forEach((stream: NodeJS.ReadableStream) => { - // try { - // logger.debug(`Destroying stats stream`); - // stream.pause(); - // stream.unpipe(); - // } catch (err) { - // logger.error(`Error destroying stream: ${err}`); - // } - // }); - // (socket as any).streams = []; - //} - - logger.info( - `Closed WebSocket (${socket.id}) - Code: ${code} - Reason: ${reason}`, - ); + // Atomic closure flag + const wasOpen = (socket as any).isOpen; + (socket as any).isOpen = false; + + // Immediate heartbeat cleanup + clearInterval((socket as any).heartbeat); + + // Force-close streams using destructor pattern + const streams = (socket as any).streams || []; + streams.forEach(({ statsStream, splitStream }) => { + try { + // Immediate pipeline breakdown + statsStream.unpipe(splitStream); + statsStream.destroy(new Error("WebSocket closed")); + splitStream.destroy(new Error("WebSocket closed")); + + // Remove all potential listeners + statsStream.removeAllListeners(); + splitStream.removeAllListeners(); + } catch (err) { + logger.error(`Stream cleanup error: ${err}`); + } + }); + + if (wasOpen) { + logger.info( + `Closed WebSocket (${socket.id}) - Code: ${code} - Reason: ${reason}`, + ); + } }, }, ); From b1aaefbf7ad228effd80304a913ab96de72fe56b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 26 Feb 2025 18:06:31 +0100 Subject: [PATCH 138/369] Fix: CLosing now works --- src/routes/docker-websocket.ts | 26 +++++++------------------- src/typings/websocket.ts | 9 +++++++++ 2 files changed, 16 insertions(+), 19 deletions(-) create mode 100644 src/typings/websocket.ts diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 09770dce..06ea1be2 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -12,6 +12,8 @@ import { responseHandler } from "~/core/utils/respone-handler"; import type { DockerHost } from "~/typings/docker"; import split2 from "split2"; import type { Readable } from "stream"; +import type internal from "stream"; +import type { streams } from "~/typings/websocket"; const set: { headers: HTTPHeaders; status?: number | keyof StatusMap } = { headers: {}, @@ -104,7 +106,6 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( splitStream.destroy(); }); - // Process data statsStream .pipe(splitStream) .on("data", (line: string) => { @@ -126,7 +127,6 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( memoryUsage, }; socket.send(JSON.stringify(data)); - logger.debug(`Parsing data on Socket ${socket.id}`); } catch (parseErr: any) { logger.error( `Failed to parse stats for container ${containerInfo.Id} on host ${host.name}: ${parseErr.message}`, @@ -137,39 +137,28 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( logger.error( `Stats stream error for container ${containerInfo.Id} on host ${host.name}: ${err.message}`, ); - const errResponse = responseHandler.error( - set, - err.message, - `Stats stream error for container ${containerInfo.Id}`, - 500, - ); if (socket.readyState === 1) { socket.send( JSON.stringify({ hostId: host.name, containerId: containerInfo.Id, - error: errResponse.error, + error: `Stats stream error for container ${containerInfo.Id} on host ${host.name}`, }), ); } statsStream.destroy(); }); } catch (streamErr: any) { + const errMsg = `Failed to start stats stream for container ${containerInfo.Id}`; logger.error( `Failed to start stats stream for container ${containerInfo.Id} on host ${host.name}: ${streamErr.message}`, ); - const errResponse = responseHandler.error( - set, - streamErr.message, - `Failed to start stats stream for container ${containerInfo.Id}`, - 500, - ); if (socket.readyState === 1) { socket.send( JSON.stringify({ hostId: host.name, containerId: containerInfo.Id, - error: errResponse.error, + error: errMsg, }), ); } @@ -198,12 +187,11 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( }, message(socket, message) { - // Handle pong responses if (message === "pong") return; }, close(socket, code, reason) { - // Atomic closure flag + logger.info(`Closing SplitStream and WebSocket (${socket.id})`); const wasOpen = (socket as any).isOpen; (socket as any).isOpen = false; @@ -211,7 +199,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( clearInterval((socket as any).heartbeat); // Force-close streams using destructor pattern - const streams = (socket as any).streams || []; + const streams: streams[] = (socket as any).streams || []; streams.forEach(({ statsStream, splitStream }) => { try { // Immediate pipeline breakdown diff --git a/src/typings/websocket.ts b/src/typings/websocket.ts new file mode 100644 index 00000000..a9712473 --- /dev/null +++ b/src/typings/websocket.ts @@ -0,0 +1,9 @@ +import type { Readable } from "stream"; +import type internal from "stream"; + +interface streams { + statsStream: Readable; + splitStream: internal.Transform; +} + +export { streams }; From 90b829e436b3da8630fabfd92fd9a97ea0649187 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 26 Feb 2025 18:29:23 +0100 Subject: [PATCH 139/369] Feat: Logger adjustments, WebSocket closing and more --- src/core/utils/logger.ts | 57 ++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index c704d0a0..b68a02f2 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -1,7 +1,5 @@ import { createLogger, format, transports } from "winston"; -import Transport from "winston-transport"; import path from "path"; -import { dbFunctions } from "~/core/database/repository"; import chalk, { ChalkInstance } from "chalk"; const fileLineFormat = format((info) => { @@ -29,44 +27,29 @@ const fileLineFormat = format((info) => { return info; }); -class SQLiteTransport extends Transport { - constructor(opts?: Transport.TransportStreamOptions) { - super(opts); - } - - log(info: any, callback: () => void) { - const { level, message, file, line } = info; - dbFunctions.addLogEntry(level, message, file || "unknown", line || 0); - callback(); - } -} - export const logger = createLogger({ level: "debug", - format: format.combine(fileLineFormat(), format.json()), - transports: [ - new transports.Console({ - format: format.combine( - format.printf(({ level, message, file, line }) => { - const levelColors: { [key: string]: ChalkInstance } = { - error: chalk.red.bold, - warn: chalk.yellow.bold, - info: chalk.green.bold, - debug: chalk.blue.bold, - verbose: chalk.cyan.bold, - silly: chalk.magenta.bold, - }; - - const paddedLevel = level.toUpperCase(); - const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); + format: format.combine( + format.timestamp({ format: "DD/MM HH:mm:ss" }), + fileLineFormat(), + format.printf(({ timestamp, level, message, file, line }) => { + const levelColors: { [key: string]: ChalkInstance } = { + error: chalk.red.bold, + warn: chalk.yellow.bold, + info: chalk.green.bold, + debug: chalk.blue.bold, + verbose: chalk.cyan.bold, + silly: chalk.magenta.bold, + }; - const coloredContext = chalk.cyan(`${file}:${line}`); - const coloredMessage = chalk.gray(message); + const paddedLevel = level.toUpperCase().padEnd(5); + const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); + const coloredContext = chalk.cyan(`${file}:${line}`); + const coloredMessage = chalk.gray(message); + const coloredTimestamp = chalk.yellow(`${timestamp}`); - return `${coloredLevel} [ ${coloredContext} ] - ${coloredMessage}`; - }), - ), + return `${coloredLevel} [ ${coloredTimestamp} ] - ${coloredMessage} - [ ${coloredContext} ]`; }), - new SQLiteTransport(), - ], + ), + transports: [new transports.Console()], }); From 36e1dc28729cf78b874e87871cf89d7abcb9438c Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 26 Feb 2025 19:27:13 +0100 Subject: [PATCH 140/369] Fix: Log level adjustment --- src/routes/docker-websocket.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 06ea1be2..f1e6e684 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -31,7 +31,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( (socket as any).streams = []; (socket as any).heartbeat = null; // Add heartbeat reference - logger.debug(`Opened WebSocket (${socket.id})`); + logger.info(`Opened WebSocket (${socket.id})`); try { hosts = dbFunctions.getDockerHosts(); From b1559c8cc320b1cccab6b361f5d5d1f4da6953b2 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 27 Feb 2025 21:13:32 +0100 Subject: [PATCH 141/369] Feat: Scheduling, Plugins (wip), databse saving of container stats --- .gitignore | 1 + package.json | 6 +- src/core/database/repository.ts | 153 +++++++++++++++++++---- src/core/docker/scheduler.ts | 72 +++++++++++ src/core/docker/store-container-stats.ts | 77 ++++++++++++ src/core/plugins/loader.ts | 21 +++- src/core/plugins/plugin-actions.ts | 10 ++ src/core/plugins/plugin-manager.ts | 13 +- src/core/utils/change-me-checker.ts | 16 +++ src/core/utils/logger.ts | 12 ++ src/core/utils/type-check.ts | 28 ----- src/index.ts | 13 +- src/plugins/telegram.plugin.ts | 33 +++++ src/routes/api-config.ts | 10 +- src/typings/database.ts | 2 + 15 files changed, 398 insertions(+), 69 deletions(-) create mode 100644 src/core/docker/scheduler.ts create mode 100644 src/core/docker/store-container-stats.ts create mode 100644 src/core/plugins/plugin-actions.ts create mode 100644 src/core/utils/change-me-checker.ts delete mode 100644 src/core/utils/type-check.ts create mode 100644 src/plugins/telegram.plugin.ts diff --git a/.gitignore b/.gitignore index 4bc7b0ad..138ece65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. *.db +*.db-journal # dependencies /node_modules diff --git a/package.json b/package.json index ff72d906..515c3010 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,6 @@ }, "dependencies": { "@elysiajs/swagger": "^1.2.2", - "@types/dockerode": "^3.3.34", - "@types/split2": "^4.2.3", "chalk": "^5.4.1", "dockerode": "^4.0.4", "elysia": "latest", @@ -17,7 +15,9 @@ "winston-transport": "^4.9.0" }, "devDependencies": { - "bun-types": "latest" + "bun-types": "latest", + "@types/dockerode": "^3.3.34", + "@types/split2": "^4.2.3" }, "module": "src/index.js", "trustedDependencies": [ diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 6c0a62b9..12c7f812 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -1,7 +1,5 @@ import Database from "bun:sqlite"; import { logger } from "~/core/utils/logger"; -import { typeCheck } from "~/core/utils/type-check"; -import { config } from "~/typings/database"; import type { DockerHost } from "~/typings/docker"; const db = new Database("dockstatapi.db"); @@ -15,8 +13,22 @@ export const dbFunctions = { secure BOOLEAN ); + CREATE TABLE IF NOT EXISTS container_stats ( + id TEXT, + hostId TEXT, + name TEXT, + image TEXT, + status TEXT, + state TEXT, + cpu_usage FLOAT, + memory_usage FLOAT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ); + CREATE TABLE IF NOT EXISTS config ( - polling_rate NUMBER + polling_rate NUMBER, + keep_data_for NUMBER, + fetching_interval NUMBER ); CREATE TABLE IF NOT EXISTS backend_log_entries ( @@ -28,25 +40,42 @@ export const dbFunctions = { ); `); + /* + * Default values: + * - Websocket polling interval 5 seconds + * - Data retention value for the database (logs and container stats) 7 days + * - Data fetcher for the Database: 5 minutes + */ const configRow = db .prepare(`SELECT COUNT(*) AS count FROM config`) .get() as { count: number }; if (configRow.count === 0) { const stmt = db.prepare( ` - INSERT INTO config (polling_rate) VALUES (5) + INSERT INTO config (polling_rate, keep_data_for, fetching_interval) VALUES (5, 7, 5) `, ); - stmt.run(); } + + const hostRow = db + .prepare(`SELECT COUNT(*) AS count FROM docker_hosts WHERE name = ?`) + .get("Localhost") as { count: number }; + if (hostRow.count === 0) { + const stmt = db.prepare( + ` + INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) + `, + ); + stmt.run("Localhost", "localhost:2375", false); + } }, addDockerHost(hostId: string, url: string, secure: boolean) { if ( - !typeCheck(hostId, "string") || - !typeCheck(url, "string") || - !typeCheck(secure, "boolean") + typeof hostId !== "string" || + typeof url !== "string" || + typeof secure !== "boolean" ) { logger.crit("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); @@ -76,10 +105,10 @@ export const dbFunctions = { line: number, ) => { if ( - !typeCheck(level, "string") || - !typeCheck(message, "string") || - !typeCheck(file_name, "string") || - !typeCheck(line, "number") + typeof level !== "string" || + typeof message !== "string" || + typeof file_name !== "string" || + typeof line !== "number" ) { logger.crit("Invalid parameter types for addLogEntry"); throw new TypeError("Invalid parameter types for addLogEntry"); @@ -102,7 +131,7 @@ export const dbFunctions = { }, getLogsByLevel(level: string) { - if (!typeCheck(level, "string")) { + if (typeof level !== "string") { logger.crit("Level parameter must be a string"); throw new TypeError("Level parameter must be a string"); } @@ -118,9 +147,9 @@ export const dbFunctions = { updateDockerHost(name: string, url: string, secure: boolean) { if ( - !typeCheck(name, "string") || - !typeCheck(url, "string") || - !typeCheck(secure, "boolean") + typeof name !== "string" || + typeof url !== "string" || + typeof secure !== "boolean" ) { logger.crit("Invalid parameter types for updateDockerHost"); throw new TypeError("Invalid parameter types for updateDockerHost"); @@ -135,7 +164,7 @@ export const dbFunctions = { }, deleteDockerHost(name: string) { - if (!typeCheck(name, "string")) { + if (typeof name !== "string") { logger.crit("Invalid parameter type for deleteDockerHost"); throw new TypeError("Name parameter must be a string"); } @@ -155,7 +184,7 @@ export const dbFunctions = { }, clearLogsByLevel(level: string) { - if (!typeCheck(level, "string")) { + if (typeof level !== "string") { logger.crit("Invalid parameter type for clearLogsByLevel"); throw new TypeError("Level parameter must be a string"); } @@ -167,28 +196,98 @@ export const dbFunctions = { return stmt.run(level); }, - updateConfig(polling_rate: number) { - if (!typeCheck(polling_rate, "number")) { - logger.crit("Invalid parameter type for updateConfig"); - throw new TypeError("Polling rate must be a number!"); + updateConfig( + polling_rate: number, + fetching_interval: number, + keep_data_for: number, + ) { + if ( + typeof polling_rate !== "number" || + typeof fetching_interval !== "number" || + typeof keep_data_for !== "number" + ) { + logger.crit("Invalid parameter types for updateConfig"); + throw new TypeError("Invalid parameter types for updateConfig"); } const stmt = db.prepare(` - UPDATE config - SET polling_rate = ? - `); + UPDATE config + SET polling_rate = ?, + fetching_interval = ?, + keep_data_for = ? + `); - return stmt.run(polling_rate); + return stmt.run(polling_rate, fetching_interval, keep_data_for); }, getConfig() { const stmt = db.prepare(` - SELECT distinct(polling_rate) + SELECT polling_rate, keep_data_for, fetching_interval FROM config `); return stmt.all(); }, + + // Stats: + addContainerStats( + id: string, + hostId: string, + name: string, + image: string, + status: string, + state: string, + cpu_usage: number, + memory_usage: number, + ) { + if ( + typeof id !== "string" || + typeof hostId !== "string" || + typeof name !== "string" || + typeof image !== "string" || + typeof status !== "string" || + typeof state !== "string" || + typeof cpu_usage !== "number" || + typeof memory_usage !== "number" + ) { + logger.crit("Invalid parameter types for addContainerStats"); + throw new TypeError("Invalid parameter types for addContainerStats"); + } + + const stmt = db.prepare(` + INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + return stmt.run( + id, + hostId, + name, + image, + status, + state, + cpu_usage, + memory_usage, + ); + }, + + deleteOldData(days: number) { + if (typeof days !== "number") { + logger.crit("Invalid parameter type for deleteOldData"); + throw new TypeError("Days parameter must be a number"); + } + + const deleteContainerStmt = db.prepare(` + DELETE FROM container_stats + WHERE timestamp < datetime('now', '-' || ? || ' days') + `); + deleteContainerStmt.run(days); + + const deleteLogsStmt = db.prepare(` + DELETE FROM backend_log_entries + WHERE timestamp < datetime('now', '-' || ? || ' days') + `); + deleteLogsStmt.run(days); + }, }; dbFunctions.init(); diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts new file mode 100644 index 00000000..14d4118e --- /dev/null +++ b/src/core/docker/scheduler.ts @@ -0,0 +1,72 @@ +import storeContainerData from "~/core/docker/store-container-stats"; +import { dbFunctions } from "../database/repository"; +import { config } from "~/typings/database"; +import { logger } from "~/core/utils/logger"; + +function convertFromMinToMs(minutes: number): number { + return minutes * 60 * 1000; +} + +async function setSchedules() { + try { + const rawConfigData: unknown[] = dbFunctions.getConfig(); + const configData = rawConfigData[0]; + + if ( + !configData || + typeof (configData as config).keep_data_for !== "number" || + typeof (configData as config).fetching_interval !== "number" + ) { + logger.error("Invalid configuration data:", configData); + throw new Error("Invalid configuration data"); + } + + const { keep_data_for, fetching_interval } = configData as config; + + if (keep_data_for === undefined) { + const errMsg = "keep_data_for is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + if (fetching_interval === undefined) { + const errMsg = "fetching_interval is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + logger.info( + `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, + ); + logger.info(`Scheduling: Cleaning up Database every ${keep_data_for} days`); + + // Schedule container data fetching + setInterval(async () => { + try { + logger.info("Task Start: Fetching container data."); + await storeContainerData(); + logger.info("Task End: Container data fetched successfully."); + } catch (error) { + logger.error("Error in fetching container data:", error); + } + }, convertFromMinToMs(fetching_interval)); + + // Schedule database cleanup + setInterval(() => { + try { + logger.info("Task Start: Cleaning up old database data."); + dbFunctions.deleteOldData(keep_data_for); + logger.info("Task End: Database cleanup completed."); + } catch (error) { + logger.error("Error in database cleanup task:", error); + } + }, convertFromMinToMs(60)); + + logger.info("Schedules have been set successfully."); + } catch (error) { + logger.error("Error setting schedules:", error); + throw error; + } +} + +export { setSchedules }; diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts new file mode 100644 index 00000000..e39d9375 --- /dev/null +++ b/src/core/docker/store-container-stats.ts @@ -0,0 +1,77 @@ +import { getDockerClient } from "~/core/docker/client"; +import { dbFunctions } from "~/core/database/repository"; +import Docker from "dockerode"; +import { + calculateCpuPercent, + calculateMemoryUsage, +} from "~/core/utils/calculations"; + +async function storeContainerData() { + try { + // Stage 1: getting all docker hosts and mapping over them + const hosts = dbFunctions.getDockerHosts(); + + hosts.map(async (host) => { + try { + // Stage 2: getting the Docker client and pinging to test the connection + const docker = getDockerClient(host); + + try { + await docker.ping(); + } catch (error) { + throw new Error( + `Error while pinging docker host: ${error as string}`, + ); + } + + const containers = await docker.listContainers({ all: true }); + + await Promise.all( + containers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return reject( + new Error(`An Error occured: ${error as string}`), + ); + } + if (!stats) { + return reject( + new Error(`No Stats available: ${error as string}`), + ); + } + resolve(stats); + }); + }, + ); + + dbFunctions.addContainerStats( + containerInfo.Id, + host.name, + containerInfo.Names[0].replace(/^\//, ""), + containerInfo.Image, + containerInfo.Status, + containerInfo.State, + calculateCpuPercent(stats), + calculateMemoryUsage(stats), + ); + } catch (error) { + throw new Error(`An error occurred: ${error as string}`); + } + }), + ); + } catch (error: unknown) { + throw new Error( + `Error while getting docker client: ${error as string}`, + ); + } + }); + } catch (error: unknown) { + throw new Error("Error while XXX"); + } +} + +export default storeContainerData; diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts index 40f79c4d..26d00e5b 100644 --- a/src/core/plugins/loader.ts +++ b/src/core/plugins/loader.ts @@ -1,21 +1,36 @@ import { pluginManager } from "./plugin-manager"; import path from "path"; import fs from "fs"; +import { logger } from "../utils/logger"; +import { checkFileForChangeMe } from "../utils/change-me-checker"; export async function loadPlugins(pluginDir: string) { const pluginPath = path.join(process.cwd(), pluginDir); + logger.debug(`Loading plugins (${pluginPath})`); if (!fs.existsSync(pluginPath)) { return; } + let pluginCount = 0; const files = fs.readdirSync(pluginPath); for (const file of files) { if (!file.endsWith(".plugin.ts")) continue; - const module = await import(path.join(pluginPath, file)); - const plugin = module.default; - pluginManager.register(plugin); + const absolutePath = path.join(pluginPath, file); + try { + await checkFileForChangeMe(absolutePath); + const module = await import(absolutePath); + const plugin = module.default; + pluginManager.register(plugin); + pluginCount++; + } catch (error) { + logger.error( + `Error while importing plugin ${absolutePath}: ${error as string}`, + ); + } } + + logger.info(`Registered ${pluginCount} plugin(s)`); } diff --git a/src/core/plugins/plugin-actions.ts b/src/core/plugins/plugin-actions.ts new file mode 100644 index 00000000..0b2f9357 --- /dev/null +++ b/src/core/plugins/plugin-actions.ts @@ -0,0 +1,10 @@ +import { pluginManager } from "./plugin-manager"; + +export const pluginAction = { + containerStart(containerInfo: any) { + pluginManager.handleContainerStart(containerInfo); + }, + metricsReceived(metrics: any) { + pluginManager.handleMetrics(metrics); + }, +}; diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index 15d66f45..2a9b1313 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -1,4 +1,5 @@ import { EventEmitter } from "events"; +import { logger } from "../utils/logger"; export interface Plugin { name: string; @@ -11,14 +12,22 @@ export class PluginManager extends EventEmitter { private plugins: Map = new Map(); register(plugin: Plugin) { - this.plugins.set(plugin.name, plugin); - console.log(`Registered plugin: ${plugin.name}`); + try { + this.plugins.set(plugin.name, plugin); + logger.debug(`Registered plugin: ${plugin.name}`); + } catch (error) { + logger.error( + `Registering plugin ${plugin.name} failed: ${error as string}`, + ); + } } unregister(name: string) { this.plugins.delete(name); } + // Trigger plugin flows: + handleContainerStart(containerInfo: any) { this.plugins.forEach((plugin) => { plugin.onContainerStart?.(containerInfo); diff --git a/src/core/utils/change-me-checker.ts b/src/core/utils/change-me-checker.ts new file mode 100644 index 00000000..fa19520b --- /dev/null +++ b/src/core/utils/change-me-checker.ts @@ -0,0 +1,16 @@ +import { readFile } from "fs/promises"; +import { logger } from "~/core/utils/logger"; + +export async function checkFileForChangeMe(filePath: string) { + const regex = /change[\W_]*me/i; + let content = ""; + try { + content = await readFile(filePath, "utf-8"); + } catch (error) { + logger.error("Error reading file:", error); + } + + if (regex.test(content)) { + throw new Error(`Error: The file contains 'CHANGE_ME'. Please update it.`); + } +} diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index b68a02f2..a173cb76 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -1,6 +1,7 @@ import { createLogger, format, transports } from "winston"; import path from "path"; import chalk, { ChalkInstance } from "chalk"; +import { dbFunctions } from "../database/repository"; const fileLineFormat = format((info) => { try { @@ -48,6 +49,17 @@ export const logger = createLogger({ const coloredMessage = chalk.gray(message); const coloredTimestamp = chalk.yellow(`${timestamp}`); + try { + dbFunctions.addLogEntry( + level, + message as string, + file as string, + line as number, + ); + } catch (error) { + logger.error(`Error inserting log into DB: ${error as string}`); + } + return `${coloredLevel} [ ${coloredTimestamp} ] - ${coloredMessage} - [ ${coloredContext} ]`; }), ), diff --git a/src/core/utils/type-check.ts b/src/core/utils/type-check.ts deleted file mode 100644 index 8675f79e..00000000 --- a/src/core/utils/type-check.ts +++ /dev/null @@ -1,28 +0,0 @@ -type TypeCheck = [any, string]; - -export function typeCheck(value: any, expectedType: string): boolean { - if (expectedType === "null") { - return value === null; - } - - if (expectedType === "array") { - return Array.isArray(value); - } - - const actualType = typeof value; - - if (actualType === "object" && value !== null) { - if (expectedType === "object") { - return !Array.isArray(value); - } - return false; - } - - return actualType === expectedType; -} - -export function validateTypes(checks: TypeCheck[]): boolean[] { - return checks.map(([value, expectedType]) => { - return typeCheck(value, expectedType.toLowerCase()); - }); -} diff --git a/src/index.ts b/src/index.ts index 06e500d6..90e7367f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,10 +8,13 @@ import { dockerStatsRoutes } from "~/routes/docker-stats"; import { backendLogs } from "~/routes/logs"; import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; import { apiConfigRoutes } from "~/routes/api-config"; +import { setSchedules } from "~/core/docker/scheduler"; + +logger.info("Starting server..."); dbFunctions.init(); -const app = new Elysia() +const DockStatAPI = new Elysia() .use( swagger({ documentation: { @@ -47,9 +50,9 @@ const app = new Elysia() async function startServer() { try { - await loadPlugins("./plugins"); + await loadPlugins("./src/plugins"); - app.listen(3000, ({ hostname, port }) => { + DockStatAPI.listen(3000, ({ hostname, port }) => { logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( `Swagger API Documentation available at http://${hostname}:${port}/swagger`, @@ -61,4 +64,6 @@ async function startServer() { } } -startServer(); +await startServer(); +await setSchedules(); +logger.info("Started server"); diff --git a/src/plugins/telegram.plugin.ts b/src/plugins/telegram.plugin.ts new file mode 100644 index 00000000..1af70f8a --- /dev/null +++ b/src/plugins/telegram.plugin.ts @@ -0,0 +1,33 @@ +import type { Plugin } from "~/core/plugins/plugin-manager"; +import { logger } from "~/core/utils/logger"; + +const TELEGRAM_BOT_TOKEN = "CHANGE_ME"; // Replace with your bot token +const TELEGRAM_CHAT_ID = "CHANGE_ME"; // Replace with your chat ID + +const TelegramNotificationPlugin: Plugin = { + name: "Telegram Notification Plugin", + async onContainerStart(containerName: string) { + const message = `Container Started: ${containerName}`; + try { + const response = await fetch( + `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + chat_id: TELEGRAM_CHAT_ID, + text: message, + }), + }, + ); + if (!response.ok) { + logger.error(`HTTP error ${response.status}`); + } + logger.info("Telegram notification sent."); + } catch (error) { + logger.error("Failed to send Telegram notification", error as string); + } + }, +}; + +export default TelegramNotificationPlugin; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 41262c85..a77d2b44 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -31,9 +31,13 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) "/update", async ({ set, body }) => { try { - const { polling_rate } = body; + const { polling_rate, fetching_interval, keep_data_for } = body; set.headers["Content-Type"] = "application/json"; - dbFunctions.updateConfig(polling_rate); + dbFunctions.updateConfig( + polling_rate, + fetching_interval, + keep_data_for, + ); return responseHandler.ok(set, "Updated DockStatAPI config"); } catch (error) { return responseHandler.error( @@ -46,6 +50,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) { body: t.Object({ polling_rate: t.Number(), + fetching_interval: t.Number(), + keep_data_for: t.Number(), }), tags: ["Management"], }, diff --git a/src/typings/database.ts b/src/typings/database.ts index d39ccbf2..425fa460 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -8,6 +8,8 @@ interface backend_log_entries { interface config { polling_rate: number; + keep_data_for: number; + fetching_interval: number; } export type { backend_log_entries, config }; From e0352f971e5c197f94be55c0087b1ac780285515 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 28 Feb 2025 21:17:32 +0100 Subject: [PATCH 142/369] Feat: README update, Storing host stats in db, plugin functions and more --- README.md | 69 +++++++++- src/core/database/repository.ts | 69 ++++++++++ src/core/docker/scheduler.ts | 47 ++++++- src/core/docker/store-container-stats.ts | 57 +++++--- src/core/docker/store-host-stats.ts | 68 ++++++++++ src/core/plugins/plugin-manager.ts | 76 +++++++++-- src/plugins/example.plugin.ts | 30 +++-- src/plugins/telegram.plugin.ts | 9 +- src/routes/docker-stats.ts | 13 +- src/typings/docker.ts | 10 +- src/typings/dockerode.ts | 162 +++++++++++++++++++++++ src/typings/plugin.ts | 25 ++++ 12 files changed, 583 insertions(+), 52 deletions(-) create mode 100644 src/core/docker/store-host-stats.ts create mode 100644 src/typings/dockerode.ts create mode 100644 src/typings/plugin.ts diff --git a/README.md b/README.md index 6cc99aff..eb71a1e4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,68 @@ -# REWRITE +# DockStat API -Using Bun, keep an eye out! +! WIP Documentation ! + +## Usage + +The DockStat API provides the following endpoints: + +### Docker Containers +- `GET /docker/containers`: Retrieve statistics for all containers across all configured Docker hosts. + +### Docker Hosts +- `GET /docker/hosts/:id`: Retrieve configuration and statistics for a specific Docker host. + +### Docker Configuration +- `POST /docker-config/add-host`: Add a new Docker host. +- `POST /docker-config/update-host`: Update an existing Docker host. +- `GET /docker-config/hosts`: Retrieve a list of all configured Docker hosts. + +### API Configuration +- `GET /config/get`: Retrieve the current API configuration. +- `POST /config/update`: Update the API configuration. + +### Logs +- `GET /logs`: Retrieve all backend logs. +- `GET /logs/:level`: Retrieve logs filtered by log level. +- `DELETE /logs`: Clear all backend logs. +- `DELETE /logs/:level`: Clear logs by log level. + +## API + +The DockStat API exposes the following endpoints: + +| Endpoint | Method | Description | +| --- | --- | --- | +| `/docker/containers` | `GET` | Retrieve statistics for all containers across all configured Docker hosts. | +| `/docker/hosts/:id` | `GET` | Retrieve configuration and statistics for a specific Docker host. | +| `/docker-config/add-host` | `POST` | Add a new Docker host. | +| `/docker-config/update-host` | `POST` | Update an existing Docker host. | +| `/docker-config/hosts` | `GET` | Retrieve a list of all configured Docker hosts. | +| `/config/get` | `GET` | Retrieve the current API configuration. | +| `/config/update` | `POST` | Update the API configuration. | +| `/logs` | `GET` | Retrieve all backend logs. | +| `/logs/:level` | `GET` | Retrieve logs filtered by log level. | +| `/logs` | `DELETE` | Clear all backend logs. | +| `/logs/:level` | `DELETE` | Clear logs by log level. | + +## Contributing + +1. Fork the repository. +2. Create a new branch for your feature or bug fix. +3. Make your changes and commit them. +4. Push your branch to your forked repository. +5. Submit a pull request to the main repository. + +## License + +This project is licensed under the [MIT License](LICENSE). + +## Testing + +To run the tests, execute the following command: +(Currently no tests configured!) +``` +bun test +``` + +This will run the test suite and report the results. diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 12c7f812..7c60a5dd 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -1,6 +1,7 @@ import Database from "bun:sqlite"; import { logger } from "~/core/utils/logger"; import type { DockerHost } from "~/typings/docker"; +import type { HostStats } from "~/typings/docker"; const db = new Database("dockstatapi.db"); @@ -13,6 +14,22 @@ export const dbFunctions = { secure BOOLEAN ); + CREATE TABLE IF NOT EXISTS host_stats ( + hostId TEXT PRIMARY KEY, + dockerVersion TEXT, + apiVersion TEXT, + os TEXT, + architecture TEXT, + totalMemory INTEGER, + totalCPU INTEGER, + labels TEXT, + containers INTEGER, + containersRunning INTEGER, + containersStopped INTEGER, + containersPaused INTEGER, + images INTEGER + ); + CREATE TABLE IF NOT EXISTS container_stats ( id TEXT, hostId TEXT, @@ -50,6 +67,7 @@ export const dbFunctions = { .prepare(`SELECT COUNT(*) AS count FROM config`) .get() as { count: number }; if (configRow.count === 0) { + logger.debug("Initializing default config"); const stmt = db.prepare( ` INSERT INTO config (polling_rate, keep_data_for, fetching_interval) VALUES (5, 7, 5) @@ -62,6 +80,7 @@ export const dbFunctions = { .prepare(`SELECT COUNT(*) AS count FROM docker_hosts WHERE name = ?`) .get("Localhost") as { count: number }; if (hostRow.count === 0) { + logger.debug("Initializing default docker host (Localhost)"); const stmt = db.prepare( ` INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) @@ -288,6 +307,56 @@ export const dbFunctions = { `); deleteLogsStmt.run(days); }, + + updateHostStats(stats: HostStats) { + const labelsJson = JSON.stringify(stats.labels); + const stmt = db.prepare(` + INSERT INTO host_stats ( + hostId, + dockerVersion, + apiVersion, + os, + architecture, + totalMemory, + totalCPU, + labels, + containers, + containersRunning, + containersStopped, + containersPaused, + images + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(hostId) DO UPDATE SET + dockerVersion = excluded.dockerVersion, + apiVersion = excluded.apiVersion, + os = excluded.os, + architecture = excluded.architecture, + totalMemory = excluded.totalMemory, + totalCPU = excluded.totalCPU, + labels = excluded.labels, + containers = excluded.containers, + containersRunning = excluded.containersRunning, + containersStopped = excluded.containersStopped, + containersPaused = excluded.containersPaused, + images = excluded.images; + `); + return stmt.run( + stats.hostId, + stats.dockerVersion, + stats.apiVersion, + stats.os, + stats.architecture, + stats.totalMemory, + stats.totalCPU, + labelsJson, + stats.containers, + stats.containersRunning, + stats.containersStopped, + stats.containersPaused, + stats.images, + ); + }, }; dbFunctions.init(); diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index 14d4118e..e4adf9ce 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -1,12 +1,30 @@ import storeContainerData from "~/core/docker/store-container-stats"; -import { dbFunctions } from "../database/repository"; +import { dbFunctions } from "~/core/database/repository"; import { config } from "~/typings/database"; import { logger } from "~/core/utils/logger"; +import storeHostData from "~/core/docker//store-host-stats"; function convertFromMinToMs(minutes: number): number { return minutes * 60 * 1000; } +async function initialRun( + scheduleName: string, + scheduleFunction: Promise | void, + isAsync: boolean, +) { + try { + if (isAsync) { + await scheduleFunction; + } else { + scheduleFunction; + } + logger.info(`Startup run success for: ${scheduleName}`); + } catch (error) { + logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); + } +} + async function setSchedules() { try { const rawConfigData: unknown[] = dbFunctions.getConfig(); @@ -38,9 +56,17 @@ async function setSchedules() { logger.info( `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, ); - logger.info(`Scheduling: Cleaning up Database every ${keep_data_for} days`); + + logger.info( + `Scheduling: Updating host statistics every ${fetching_interval} minutes`, + ); + + logger.info( + `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, + ); // Schedule container data fetching + await initialRun("storeContainerData", storeContainerData(), true); setInterval(async () => { try { logger.info("Task Start: Fetching container data."); @@ -51,7 +77,24 @@ async function setSchedules() { } }, convertFromMinToMs(fetching_interval)); + // Schedule Host statistics updates + await initialRun("storeHostData", storeHostData(), true); + setInterval(async () => { + try { + logger.info("Task Start: Updating host stats."); + await storeHostData(); + logger.info("Task End: Updating host stats successfully."); + } catch (error) { + logger.error("Error in updating host stats:", error); + } + }, convertFromMinToMs(fetching_interval)); + // Schedule database cleanup + await initialRun( + "dbFunctions.deleteOldData", + dbFunctions.deleteOldData(keep_data_for), + false, + ); setInterval(() => { try { logger.info("Task Start: Cleaning up old database data."); diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts index e39d9375..e64f31bc 100644 --- a/src/core/docker/store-container-stats.ts +++ b/src/core/docker/store-container-stats.ts @@ -8,39 +8,57 @@ import { async function storeContainerData() { try { - // Stage 1: getting all docker hosts and mapping over them const hosts = dbFunctions.getDockerHosts(); - hosts.map(async (host) => { - try { - // Stage 2: getting the Docker client and pinging to test the connection + // Process each host concurrently and wait for them all to finish + await Promise.all( + hosts.map(async (host) => { const docker = getDockerClient(host); + // Test the connection with a ping try { await docker.ping(); } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); throw new Error( - `Error while pinging docker host: ${error as string}`, + `Failed to ping docker host "${host.name}": ${errMsg}`, ); } - const containers = await docker.listContainers({ all: true }); + let containers; + try { + containers = await docker.listContainers({ all: true }); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to list containers on host "${host.name}": ${errMsg}`, + ); + } + // Process each container concurrently await Promise.all( containers.map(async (containerInfo) => { + const containerName = containerInfo.Names[0].replace(/^\//, ""); try { const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( + + const stats: Docker.ContainerStats = await new Promise( (resolve, reject) => { container.stats({ stream: false }, (error, stats) => { if (error) { + const errMsg = + error instanceof Error ? error.message : String(error); return reject( - new Error(`An Error occured: ${error as string}`), + new Error( + `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, + ), ); } if (!stats) { return reject( - new Error(`No Stats available: ${error as string}`), + new Error( + `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".`, + ), ); } resolve(stats); @@ -51,7 +69,7 @@ async function storeContainerData() { dbFunctions.addContainerStats( containerInfo.Id, host.name, - containerInfo.Names[0].replace(/^\//, ""), + containerName, containerInfo.Image, containerInfo.Status, containerInfo.State, @@ -59,18 +77,19 @@ async function storeContainerData() { calculateMemoryUsage(stats), ); } catch (error) { - throw new Error(`An error occurred: ${error as string}`); + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error( + `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, + ); } }), ); - } catch (error: unknown) { - throw new Error( - `Error while getting docker client: ${error as string}`, - ); - } - }); - } catch (error: unknown) { - throw new Error("Error while XXX"); + }), + ); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to store container data: ${errMsg}`); } } diff --git a/src/core/docker/store-host-stats.ts b/src/core/docker/store-host-stats.ts new file mode 100644 index 00000000..f8cd3527 --- /dev/null +++ b/src/core/docker/store-host-stats.ts @@ -0,0 +1,68 @@ +import { logger } from "~/core/utils/logger"; +import { dbFunctions } from "~/core/database/repository"; +import { DockerHost, HostStats } from "~/typings/docker"; +import { getDockerClient } from "~/core/docker/client"; +import { DockerInfo } from "~/typings/dockerode"; + +async function storeHostData() { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + await Promise.all( + hosts.map(async (host) => { + const docker = getDockerClient(host); + + try { + await docker.ping(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to ping docker host "${host.name}": ${errMsg}`, + ); + } + + let hostStats: DockerInfo; + let stats: HostStats; + try { + hostStats = await docker.info(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to fetch stats for host "${host.name}": ${errMsg}`, + ); + } + + try { + const stats: HostStats = { + hostId: host.name, + dockerVersion: hostStats.ServerVersion, + apiVersion: hostStats.Driver, + os: hostStats.OperatingSystem, + architecture: hostStats.Architecture, + totalMemory: hostStats.MemTotal, + totalCPU: hostStats.NCPU, + labels: hostStats.Labels, + images: hostStats.Images, + containers: hostStats.Containers, + containersPaused: hostStats.ContainersPaused, + containersRunning: hostStats.ContainersRunning, + containersStopped: hostStats.ContainersStopped, + }; + + dbFunctions.updateHostStats(stats); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to store stats for host "${host.name}": ${errMsg}`, + ); + } + }), + ); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + logger.error(`storeHostData failed: ${errMsg}`); + throw new Error(`Failed to store host data: ${errMsg}`); + } +} + +export default storeHostData; diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index 2a9b1313..614604af 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -1,12 +1,7 @@ import { EventEmitter } from "events"; import { logger } from "../utils/logger"; - -export interface Plugin { - name: string; - onContainerStart?: (containerInfo: any) => void; - onMetricsReceived?: (metrics: any) => void; - onLogReceived?: (log: string) => void; -} +import type { Plugin } from "~/typings/plugin"; +import type { ContainerInfo, HostStats } from "~/typings/docker"; export class PluginManager extends EventEmitter { private plugins: Map = new Map(); @@ -27,16 +22,75 @@ export class PluginManager extends EventEmitter { } // Trigger plugin flows: + handleContainerStop(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerStop?.(containerInfo); + }); + } + + handleContainerExit(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerExit?.(containerInfo); + }); + } + + handleContainerCreate(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerCreate?.(containerInfo); + }); + } + + handleContainerDestroy(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerDestroy?.(containerInfo); + }); + } + + handleContainerPause(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerPause?.(containerInfo); + }); + } + + handleContainerUnpause(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerUnpause?.(containerInfo); + }); + } + + handleContainerRestart(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerRestart?.(containerInfo); + }); + } + + handleContainerUpdate(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerUpdate?.(containerInfo); + }); + } + + handleContainerRename(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerRename?.(containerInfo); + }); + } + + handleContainerHealthStatus(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerHealthStatus?.(containerInfo); + }); + } - handleContainerStart(containerInfo: any) { + handleHostUnreachable(HostStats: HostStats) { this.plugins.forEach((plugin) => { - plugin.onContainerStart?.(containerInfo); + plugin.onHostUnreachable?.(HostStats); }); } - handleMetrics(metrics: any) { + handleHostReachableAgain(HostStats: HostStats) { this.plugins.forEach((plugin) => { - plugin.onMetricsReceived?.(metrics); + plugin.onHostReachableAgain?.(HostStats); }); } } diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index 48ca11a5..a9ed6acc 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -1,11 +1,23 @@ -import { Plugin } from "~/core/plugins/plugin-manager"; +import type { Plugin } from "~/typings/plugin"; +import type { ContainerInfo } from "~/typings/docker"; +import type { HostStats } from "~/typings/docker"; +import { logger } from "~/core/utils/logger"; -export default { - name: "example-plugin", - onContainerStart: (containerInfo) => { - console.log(`Container started: ${containerInfo.id}`); - }, - onMetricsReceived: (metrics) => { - console.log("Received metrics:", metrics); - }, +const ExamplePlugin: Plugin = { + name: "Example Plugin", + async onContainerStart(containerInfo: ContainerInfo) {}, + async onContainerStop(containerInfo: ContainerInfo) {}, + async onContainerExit(containerInfo: ContainerInfo) {}, + async onContainerCreate(containerInfo: ContainerInfo) {}, + async onContainerDestroy(containerInfo: ContainerInfo) {}, + async onContainerPause(containerInfo: ContainerInfo) {}, + async onContainerUnpause(containerInfo: ContainerInfo) {}, + async onContainerRestart(containerInfo: ContainerInfo) {}, + async onContainerUpdate(containerInfo: ContainerInfo) {}, + async onContainerRename(containerInfo: ContainerInfo) {}, + async onContainerHealthStatus(containerInfo: ContainerInfo) {}, + async onHostUnreachable(HostStats: HostStats) {}, + async onHostReachableAgain(HostStats: HostStats) {}, } satisfies Plugin; + +export default ExamplePlugin; diff --git a/src/plugins/telegram.plugin.ts b/src/plugins/telegram.plugin.ts index 1af70f8a..cf7c376d 100644 --- a/src/plugins/telegram.plugin.ts +++ b/src/plugins/telegram.plugin.ts @@ -1,4 +1,5 @@ -import type { Plugin } from "~/core/plugins/plugin-manager"; +import type { Plugin } from "~/typings/plugin"; +import type { ContainerInfo } from "~/typings/docker"; import { logger } from "~/core/utils/logger"; const TELEGRAM_BOT_TOKEN = "CHANGE_ME"; // Replace with your bot token @@ -6,8 +7,8 @@ const TELEGRAM_CHAT_ID = "CHANGE_ME"; // Replace with your chat ID const TelegramNotificationPlugin: Plugin = { name: "Telegram Notification Plugin", - async onContainerStart(containerName: string) { - const message = `Container Started: ${containerName}`; + async onContainerStart(containerInfo: ContainerInfo) { + const message = `Container Started: ${containerInfo.name} on ${containerInfo.hostId}`; try { const response = await fetch( `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, @@ -28,6 +29,6 @@ const TelegramNotificationPlugin: Plugin = { logger.error("Failed to send Telegram notification", error as string); } }, -}; +} satisfies Plugin; export default TelegramNotificationPlugin; diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 95e4a8b1..d85bfc1f 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -8,7 +8,8 @@ import { } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/respone-handler"; -import type { ContainerInfo, DockerHost, HostConfig } from "~/typings/docker"; +import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; +import type { DockerInfo } from "~/typings/dockerode"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) .get( @@ -119,9 +120,9 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) } const docker = getDockerClient(host); - const info = await docker.info(); + const info: DockerInfo = await docker.info(); - const config: HostConfig = { + const config: HostStats = { hostId: host.name, dockerVersion: info.ServerVersion, apiVersion: info.Driver, @@ -129,6 +130,12 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) architecture: info.Architecture, totalMemory: info.MemTotal, totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, }; set.headers["Content-Type"] = "application/json"; diff --git a/src/typings/docker.ts b/src/typings/docker.ts index 8d78ae2b..522762c2 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -15,7 +15,7 @@ interface ContainerInfo { memoryUsage: number; } -interface HostConfig { +interface HostStats { hostId: string; dockerVersion: string; apiVersion: string; @@ -23,6 +23,12 @@ interface HostConfig { architecture: string; totalMemory: number; totalCPU: number; + labels: string[]; + containers: number; + containersRunning: number; + containersStopped: number; + containersPaused: number; + images: number; } -export type { HostConfig, ContainerInfo, DockerHost }; +export type { HostStats, ContainerInfo, DockerHost }; diff --git a/src/typings/dockerode.ts b/src/typings/dockerode.ts new file mode 100644 index 00000000..a4604337 --- /dev/null +++ b/src/typings/dockerode.ts @@ -0,0 +1,162 @@ +interface DockerInfo { + ID: string; + Containers: number; + ContainersRunning: number; + ContainersPaused: number; + ContainersStopped: number; + Images: number; + Driver: string; + DriverStatus: [string, string][]; + DockerRootDir: string; + SystemStatus: [string, string][]; + Plugins: { + Volume: string[]; + Network: string[]; + Authorization: string[]; + Log: string[]; + }; + MemoryLimit: boolean; + SwapLimit: boolean; + KernelMemory: boolean; + CpuCfsPeriod: boolean; + CpuCfsQuota: boolean; + CPUShares: boolean; + CPUSet: boolean; + OomKillDisable: boolean; + IPv4Forwarding: boolean; + BridgeNfIptables: boolean; + BridgeNfIp6tables: boolean; + Debug: boolean; + NFd: number; + NGoroutines: number; + SystemTime: string; + LoggingDriver: string; + CgroupDriver: string; + NEventsListener: number; + KernelVersion: string; + OperatingSystem: string; + OSType: string; + Architecture: string; + NCPU: number; + MemTotal: number; + IndexServerAddress: string; + RegistryConfig: { + AllowNondistributableArtifactsCIDRs: string[]; + AllowNondistributableArtifactsHostnames: string[]; + InsecureRegistryCIDRs: string[]; + IndexConfigs: Record< + string, + { + Name: string; + Mirrors: string[]; + Secure: boolean; + Official: boolean; + } + >; + Mirrors: string[]; + }; + GenericResources: Array< + | { DiscreteResourceSpec: { Kind: string; Value: number } } + | { NamedResourceSpec: { Kind: string; Value: string } } + >; + HttpProxy: string; + HttpsProxy: string; + NoProxy: string; + Name: string; + Labels: string[]; + ExperimentalBuild: boolean; + ServerVersion: string; + ClusterStore: string; + ClusterAdvertise: string; + Runtimes: Record< + string, + { + path: string; + runtimeArgs?: string[]; + } + >; + DefaultRuntime: string; + Swarm: { + NodeID: string; + NodeAddr: string; + LocalNodeState: string; + ControlAvailable: boolean; + Error: string; + RemoteManagers: Array<{ + NodeID: string; + Addr: string; + }>; + Nodes: number; + Managers: number; + Cluster: { + ID: string; + Version: { + Index: number; + }; + CreatedAt: string; + UpdatedAt: string; + Spec: { + Name: string; + Labels: Record; + Orchestration: { + TaskHistoryRetentionLimit: number; + }; + Raft: { + SnapshotInterval: number; + KeepOldSnapshots: number; + LogEntriesForSlowFollowers: number; + ElectionTick: number; + HeartbeatTick: number; + }; + Dispatcher: { + HeartbeatPeriod: number; + }; + CAConfig: { + NodeCertExpiry: number; + ExternalCAs: Array<{ + Protocol: string; + URL: string; + Options: Record; + CACert: string; + }>; + SigningCACert: string; + SigningCAKey: string; + ForceRotate: number; + }; + EncryptionConfig: { + AutoLockManagers: boolean; + }; + TaskDefaults: { + LogDriver: { + Name: string; + Options: Record; + }; + }; + }; + TLSInfo: { + TrustRoot: string; + CertIssuerSubject: string; + CertIssuerPublicKey: string; + }; + RootRotationInProgress: boolean; + }; + }; + LiveRestoreEnabled: boolean; + Isolation: string; + InitBinary: string; + ContainerdCommit: { + ID: string; + Expected: string; + }; + RuncCommit: { + ID: string; + Expected: string; + }; + InitCommit: { + ID: string; + Expected: string; + }; + SecurityOptions: string[]; +} + +export type { DockerInfo }; diff --git a/src/typings/plugin.ts b/src/typings/plugin.ts new file mode 100644 index 00000000..9994ea67 --- /dev/null +++ b/src/typings/plugin.ts @@ -0,0 +1,25 @@ +import { ContainerInfo } from "~/typings/docker"; +import { HostStats } from "~/typings/docker"; + +interface Plugin { + name: string; + + // Container lifecycle hooks + onContainerStart?: (containerInfo: ContainerInfo) => void; + onContainerStop?: (containerInfo: ContainerInfo) => void; + onContainerExit?: (containerInfo: ContainerInfo) => void; + onContainerCreate?: (containerInfo: ContainerInfo) => void; + onContainerDestroy?: (containerInfo: ContainerInfo) => void; + onContainerPause?: (containerInfo: ContainerInfo) => void; + onContainerUnpause?: (containerInfo: ContainerInfo) => void; + onContainerRestart?: (containerInfo: ContainerInfo) => void; + onContainerUpdate?: (containerInfo: ContainerInfo) => void; + onContainerRename?: (containerInfo: ContainerInfo) => void; + onContainerHealthStatus?: (containerInfo: ContainerInfo) => void; + + // Host lifecycle hooks + onHostUnreachable?: (HostStats: HostStats) => void; + onHostReachableAgain?: (HostStats: HostStats) => void; +} + +export type { Plugin }; From ea8da8fb7dc60f10f27778ab69242122c70f7893 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 4 Mar 2025 22:34:03 +0100 Subject: [PATCH 143/369] Feat: Better logging usability, TASK level logs, database time needed in logs and Error Page --- bun.lock | 67 ++++++++++++++++--- package.json | 11 ++-- public/404.html | 110 ++++++++++++++++++++++++++++++++ public/DockStat.png | Bin 0 -> 79885 bytes src/core/database/repository.ts | 91 ++++++++++++++++++++++---- src/core/utils/logger.ts | 79 ++++++++++++++++++----- src/index.ts | 17 ++++- 7 files changed, 329 insertions(+), 46 deletions(-) create mode 100644 public/404.html create mode 100644 public/DockStat.png diff --git a/bun.lock b/bun.lock index 2c9571a8..c03c8c92 100644 --- a/bun.lock +++ b/bun.lock @@ -4,9 +4,8 @@ "": { "name": "dockstatapi", "dependencies": { + "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", - "@types/dockerode": "^3.3.34", - "@types/split2": "^4.2.3", "chalk": "^5.4.1", "dockerode": "^4.0.4", "elysia": "latest", @@ -15,7 +14,11 @@ "winston-transport": "^4.9.0", }, "devDependencies": { + "@types/dockerode": "^3.3.34", + "@types/split2": "^4.2.3", "bun-types": "latest", + "cross-env": "^7.0.3", + "wrap-ansi": "^9.0.0", }, }, }, @@ -29,6 +32,8 @@ "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], + "@elysiajs/static": ["@elysiajs/static@1.2.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-oLpAi8c+maPpA0XhhK3BELaIjIG+nXg/K9p8cFfW4q5ayRD59a3MOMOOGgpiXZkHJzLPWcouhhyyLAYtaANW4g=="], + "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], "@grpc/grpc-js": ["@grpc/grpc-js@1.12.6", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q=="], @@ -81,9 +86,9 @@ "@unhead/schema": ["@unhead/schema@1.11.19", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], @@ -107,6 +112,8 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], @@ -121,6 +128,10 @@ "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], + "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], @@ -129,7 +140,7 @@ "elysia": ["elysia@1.2.21", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-E9b1JcB7fiQ2ptk24W8OnBrMYUoKzffIXob9uTVUKhqOKxaXAd9UyWBeyr7JCDa/VD/b/9S8aIey9/YJsK5sLg=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], @@ -145,6 +156,8 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -157,6 +170,8 @@ "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], @@ -173,12 +188,16 @@ "nan": ["nan@2.22.1", "", {}, "sha512-pfRR4ZcNTSm2ZFHaztuvbICf+hyiG6ecA06SfAxoPmuHjvMu0KUIae7Y8GyVkbBqeEIidsmXeYooWIX9+qjfRQ=="], + "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], @@ -195,6 +214,10 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], @@ -205,11 +228,11 @@ "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "tar-fs": ["tar-fs@2.0.1", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.0.0" } }, "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA=="], @@ -221,17 +244,21 @@ "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -247,12 +274,32 @@ "@types/ssh2/@types/node": ["@types/node@18.19.76", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw=="], - "ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.8", "", {}, "sha512-iufA5/6hPCmRIVD2eh7qGpoKvoA08Gw/qUb2JECifBtAwA93fo7+1k9uHK440f2LMJsbxIzA+nv7RS0BmfiO/g=="], "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - "ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], } } diff --git a/package.json b/package.json index 515c3010..c6f6f741 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,10 @@ "version": "2.1.0", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "docker compose -f ./docker/docker-compose.dev.yaml up -d && bun run --watch src/index.ts" + "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts" }, "dependencies": { + "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "chalk": "^5.4.1", "dockerode": "^4.0.4", @@ -15,12 +16,14 @@ "winston-transport": "^4.9.0" }, "devDependencies": { - "bun-types": "latest", "@types/dockerode": "^3.3.34", - "@types/split2": "^4.2.3" + "@types/split2": "^4.2.3", + "bun-types": "latest", + "cross-env": "^7.0.3", + "wrap-ansi": "^9.0.0" }, "module": "src/index.js", "trustedDependencies": [ "protobufjs" ] -} +} \ No newline at end of file diff --git a/public/404.html b/public/404.html new file mode 100644 index 00000000..a0169cf7 --- /dev/null +++ b/public/404.html @@ -0,0 +1,110 @@ + + + + + + + 404 - Page Not Found + + + + +
+ +
404
+
+ Oops! The page you're looking for doesn't exist. +
+
+ + +
+
+ + + \ No newline at end of file diff --git a/public/DockStat.png b/public/DockStat.png new file mode 100644 index 0000000000000000000000000000000000000000..d375bd49107c79a960488d6062276a72cf6bd512 GIT binary patch literal 79885 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⪼EggaS+_T%ej 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 KU+70|um2a&GMD85 literal 0 HcmV?d00001 diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 7c60a5dd..bb43497f 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -7,6 +7,8 @@ const db = new Database("dockstatapi.db"); export const dbFunctions = { init() { + const startTime = Date.now(); + logger.debug("__task__ __db__ Initializing Database ⏳") db.exec(` CREATE TABLE IF NOT EXISTS docker_hosts ( name TEXT, @@ -88,9 +90,13 @@ export const dbFunctions = { ); stmt.run("Localhost", "localhost:2375", false); } + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Initializing Database ✔️ (${duration}ms)`); }, addDockerHost(hostId: string, url: string, secure: boolean) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Adding Docker Host ⏳") if ( typeof hostId !== "string" || typeof url !== "string" || @@ -104,16 +110,23 @@ export const dbFunctions = { INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) `); - return stmt.run(hostId, url, secure); + const data = stmt.run(hostId, url, secure); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Adding Docker Host ✔️ (${duration}ms)`); + return data }, getDockerHosts(): DockerHost[] { + const startTime = Date.now(); + logger.debug("__task__ __db__ Getting Docker Host ⏳") const stmt = db.prepare(` SELECT name, url, secure FROM docker_hosts ORDER BY name DESC `); const data = stmt.all(); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Getting Docker Host ✔️ (${duration}ms)`); return data as DockerHost[]; }, @@ -141,15 +154,22 @@ export const dbFunctions = { }, getAllLogs() { + const startTime = Date.now(); + logger.debug("__task__ __db__ Getting all Logs ⏳") const stmt = db.prepare(` SELECT timestamp, level, message, file, line FROM backend_log_entries ORDER BY timestamp DESC `); - return stmt.all(); + const data = stmt.all(); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Getting all Logs ✔️ (${duration}ms)`); + return data }, getLogsByLevel(level: string) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Getting level-logs ⏳") if (typeof level !== "string") { logger.crit("Level parameter must be a string"); throw new TypeError("Level parameter must be a string"); @@ -161,10 +181,15 @@ export const dbFunctions = { WHERE level = ? ORDER BY timestamp DESC `); - return stmt.all(level); + const data = stmt.all(level); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Getting level-logs ✔️ (${duration}ms)`); + return data }, updateDockerHost(name: string, url: string, secure: boolean) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Updating Docker Host ⏳") if ( typeof name !== "string" || typeof url !== "string" || @@ -179,10 +204,15 @@ export const dbFunctions = { SET url = ?, secure = ? WHERE name = ? `); - return stmt.run(url, secure, name); + const data = stmt.run(url, secure, name); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Updating Docker Host ✔️ (${duration}ms)`); + return data }, deleteDockerHost(name: string) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Deleting Docker Host ⏳") if (typeof name !== "string") { logger.crit("Invalid parameter type for deleteDockerHost"); throw new TypeError("Name parameter must be a string"); @@ -192,17 +222,27 @@ export const dbFunctions = { DELETE FROM docker_hosts WHERE name = ? `); - return stmt.run(name); + const data = stmt.run(name); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Deleting Docker Host ✔️ (${duration}ms)`); + return data }, clearAllLogs() { + const startTime = Date.now(); + logger.debug("__task__ __db__ Clearing all Logs ⏳") const stmt = db.prepare(` DELETE FROM backend_log_entries `); - return stmt.run(); + const data = stmt.run(); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Clearing all Logs ✔️ (${duration}ms)`); + return data }, clearLogsByLevel(level: string) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Clearing all logs by level ⏳") if (typeof level !== "string") { logger.crit("Invalid parameter type for clearLogsByLevel"); throw new TypeError("Level parameter must be a string"); @@ -212,7 +252,10 @@ export const dbFunctions = { DELETE FROM backend_log_entries WHERE level = ? `); - return stmt.run(level); + const data = stmt.run(level); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Clearing all logs by level ✔️ (${duration}ms)`); + return data }, updateConfig( @@ -220,6 +263,8 @@ export const dbFunctions = { fetching_interval: number, keep_data_for: number, ) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Updating config ⏳") if ( typeof polling_rate !== "number" || typeof fetching_interval !== "number" || @@ -236,16 +281,24 @@ export const dbFunctions = { keep_data_for = ? `); - return stmt.run(polling_rate, fetching_interval, keep_data_for); + const data = stmt.run(polling_rate, fetching_interval, keep_data_for); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Updating config ✔️ (${duration}ms)`); + return data }, getConfig() { + const startTime = Date.now(); + logger.debug("__task__ __db__ Getting config ⏳") const stmt = db.prepare(` SELECT polling_rate, keep_data_for, fetching_interval FROM config `); - return stmt.all(); + const data = stmt.all(); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Getting config ✔️ (${duration}ms)`); + return data }, // Stats: @@ -259,6 +312,8 @@ export const dbFunctions = { cpu_usage: number, memory_usage: number, ) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Adding container statistics ⏳") if ( typeof id !== "string" || typeof hostId !== "string" || @@ -277,7 +332,7 @@ export const dbFunctions = { INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); - return stmt.run( + const data = stmt.run( id, hostId, name, @@ -287,9 +342,14 @@ export const dbFunctions = { cpu_usage, memory_usage, ); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Adding container statistics ✔️ (${duration}ms)`); + return data }, deleteOldData(days: number) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Deleting old data ⏳") if (typeof days !== "number") { logger.crit("Invalid parameter type for deleteOldData"); throw new TypeError("Days parameter must be a number"); @@ -306,9 +366,13 @@ export const dbFunctions = { WHERE timestamp < datetime('now', '-' || ? || ' days') `); deleteLogsStmt.run(days); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Deleting old data ✔️ (${duration}ms)`); }, updateHostStats(stats: HostStats) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Update Host Stats ⏳") const labelsJson = JSON.stringify(stats.labels); const stmt = db.prepare(` INSERT INTO host_stats ( @@ -341,7 +405,7 @@ export const dbFunctions = { containersPaused = excluded.containersPaused, images = excluded.images; `); - return stmt.run( + const data = stmt.run( stats.hostId, stats.dockerVersion, stats.apiVersion, @@ -356,7 +420,8 @@ export const dbFunctions = { stats.containersPaused, stats.images, ); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Update Host stats ✔️ (${duration}ms)`); + return data }, }; - -dbFunctions.init(); diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index a173cb76..013de16d 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -2,6 +2,10 @@ import { createLogger, format, transports } from "winston"; import path from "path"; import chalk, { ChalkInstance } from "chalk"; import { dbFunctions } from "../database/repository"; +import wrapAnsi from "wrap-ansi"; + +// Change to false here if dont want the spacing on a wrapped line +const padNewlines: boolean = true; const fileLineFormat = format((info) => { try { @@ -9,59 +13,100 @@ const fileLineFormat = format((info) => { if (stack) { for (let i = 2; i < stack.length; i++) { const line = stack[i].trim(); - if ( - !line.includes("node_modules") && - !line.includes(path.basename(__filename)) - ) { + // Exclude lines from node_modules or the current file + if (!line.includes("node_modules") && !line.includes(path.basename(__filename))) { const matches = line.match(/\(?(.+):(\d+):(\d+)\)?$/); if (matches) { info.file = path.basename(matches[1]); - info.line = parseInt(matches[2]); + info.line = parseInt(matches[2], 10); break; } } } } } catch (err) { - // Ignore errors in case stack trace parsing fails + // Ignore errors during stack trace extraction } return info; }); +const formatTerminalMessage = (message: string, prefixLength: number) => { + const maxWidth = process.stdout.columns || 80; + const wrapWidth = maxWidth - prefixLength - 15; + + if (padNewlines) { + const wrapped = wrapAnsi(chalk.gray(message), wrapWidth, { + trim: true, + hard: true, + }); + + return wrapped + .split("\n") + .map((line, i) => (i === 0 ? line : " ".repeat(prefixLength) + line)) + .join("\n"); + } + return message; +}; + export const logger = createLogger({ level: "debug", format: format.combine( format.timestamp({ format: "DD/MM HH:mm:ss" }), fileLineFormat(), format.printf(({ timestamp, level, message, file, line }) => { - const levelColors: { [key: string]: ChalkInstance } = { + const levelColors: Record = { error: chalk.red.bold, warn: chalk.yellow.bold, info: chalk.green.bold, debug: chalk.blue.bold, verbose: chalk.cyan.bold, silly: chalk.magenta.bold, + task: chalk.cyan.bold }; + if ((message as string).startsWith("__task__")) { + message = (message as string).replaceAll("__task__", "").trimStart(); + level = "task" + if ((message as string).startsWith("__db__")) { + message = (message as string).replaceAll("__db__", "").trimStart(); + message = `${chalk.magenta("DB")} ${message}` + } + } + const paddedLevel = level.toUpperCase().padEnd(5); const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); - const coloredContext = chalk.cyan(`${file}:${line}`); - const coloredMessage = chalk.gray(message); - const coloredTimestamp = chalk.yellow(`${timestamp}`); + const coloredContext = chalk.cyan(`${file as string}:${line as number}`); + const coloredTimestamp = chalk.yellow(timestamp); + const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; try { dbFunctions.addLogEntry( - level, - message as string, - file as string, - line as number, + (level as string).replace(ansiRegex, ''), + (message as string).replace(ansiRegex, ''), + (file as string).replace(ansiRegex, ''), + line as number ); } catch (error) { - logger.error(`Error inserting log into DB: ${error as string}`); + // Use console.error to avoid recursive logging + console.error(`Error inserting log into DB: ${String(error)}`); + console.error("Aborting due to risk of recursion!") + process.abort() + } + + if (process.env.NODE_ENV !== "dev") { + return `${coloredLevel} [ ${coloredTimestamp} ] - ${chalk.gray( + message + )} - [ ${coloredContext} ]`; } - return `${coloredLevel} [ ${coloredTimestamp} ] - ${coloredMessage} - [ ${coloredContext} ]`; - }), + const prefix = `${paddedLevel} [ ${timestamp} ] - `; + const prefixLength = prefix.length; + const formattedMessage = formatTerminalMessage( + message as string, + prefixLength + ); + return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; + }) ), transports: [new transports.Console()], }); diff --git a/src/index.ts b/src/index.ts index 90e7367f..8758644b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,12 +9,15 @@ import { backendLogs } from "~/routes/logs"; import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; import { apiConfigRoutes } from "~/routes/api-config"; import { setSchedules } from "~/core/docker/scheduler"; +import staticPlugin from "@elysiajs/static"; +console.log("") logger.info("Starting server..."); dbFunctions.init(); const DockStatAPI = new Elysia() + .use(staticPlugin()) .use( swagger({ documentation: { @@ -46,12 +49,20 @@ const DockStatAPI = new Elysia() .use(backendLogs) .use(dockerWebsocketRoutes) .use(apiConfigRoutes) - .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }); + .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) + .onError(({ code, set }) => { + if (code === 'NOT_FOUND') { + logger.warn("Unknown route, showing error page!") + set.status = 404 + set.headers['Content-Type'] = 'text/html' + return Bun.file('public/404.html') + } + }); async function startServer() { try { - await loadPlugins("./src/plugins"); + await loadPlugins("./src/plugins"); DockStatAPI.listen(3000, ({ hostname, port }) => { logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( @@ -66,4 +77,6 @@ async function startServer() { await startServer(); await setSchedules(); + logger.info("Started server"); +console.log("") From 68ec16804a3d4013ef35bd14f214174d91f75558 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 4 Mar 2025 23:11:25 +0100 Subject: [PATCH 144/369] Feat: Adjustable log level and minor changes --- README.md | 3 +++ package.json | 6 ++++-- src/core/utils/logger.ts | 2 +- src/index.ts | 4 ++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index eb71a1e4..656260be 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ The DockStat API provides the following endpoints: - `DELETE /logs`: Clear all backend logs. - `DELETE /logs/:level`: Clear logs by log level. +### Webocket +- `WS(S) /docker/stats`: Retrieve the current API configuration. + ## API The DockStat API exposes the following endpoints: diff --git a/package.json b/package.json index c6f6f741..d8460bd4 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,10 @@ "name": "dockstatapi", "version": "2.1.0", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts" + "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", + "start:linux": "NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", + "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", + "build": "bun build --target bun src/index.ts --outdir ./dist" }, "dependencies": { "@elysiajs/static": "^1.2.0", diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 013de16d..82dc7894 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -49,7 +49,7 @@ const formatTerminalMessage = (message: string, prefixLength: number) => { }; export const logger = createLogger({ - level: "debug", + level: process.env.LOG_LEVEL || 'debug', format: format.combine( format.timestamp({ format: "DD/MM HH:mm:ss" }), fileLineFormat(), diff --git a/src/index.ts b/src/index.ts index 8758644b..3a4521f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,9 +61,9 @@ const DockStatAPI = new Elysia() async function startServer() { try { - await loadPlugins("./src/plugins"); DockStatAPI.listen(3000, ({ hostname, port }) => { + console.log("") logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( `Swagger API Documentation available at http://${hostname}:${port}/swagger`, @@ -75,8 +75,8 @@ async function startServer() { } } -await startServer(); await setSchedules(); +await startServer(); logger.info("Started server"); console.log("") From 4f2be554924512e1a70b53a0993a10856b208906 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 8 Mar 2025 16:48:19 +0100 Subject: [PATCH 145/369] Feat: Package info, contributers, License and README update --- LICENSE | 407 +++++++++++++++++++++++++++++++++ README.md | 3 +- package.json | 10 +- src/core/utils/package-json.ts | 23 ++ src/routes/api-config.ts | 37 ++- tsconfig.json | 2 +- 6 files changed, 478 insertions(+), 4 deletions(-) create mode 100644 LICENSE create mode 100644 src/core/utils/package-json.ts diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..428e5953 --- /dev/null +++ b/LICENSE @@ -0,0 +1,407 @@ +Attribution-NonCommercial 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + j. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + k. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + l. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce, reproduce, and Share Adapted Material for + NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the "Licensor." The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/README.md b/README.md index 656260be..9784f980 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ The DockStat API exposes the following endpoints: ## License -This project is licensed under the [MIT License](LICENSE). +This project is licensed under the CC BY-NC 4.0 License. +[cc-by-nc-image]: https://licensebuttons.net/l/by-nc/4.0/88x31.png ## Testing diff --git a/package.json b/package.json index d8460bd4..d28b22ad 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,13 @@ { "name": "dockstatapi", + "author": { + "email": "info@itsnik.de", + "name": "ItsNik", + "url": "https://github.com/Its4Nik" + }, + "license": "CC BY-NC 4.0", + "contributors": [], + "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", "version": "2.1.0", "scripts": { "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", @@ -28,4 +36,4 @@ "trustedDependencies": [ "protobufjs" ] -} \ No newline at end of file +} diff --git a/src/core/utils/package-json.ts b/src/core/utils/package-json.ts new file mode 100644 index 00000000..5b84757d --- /dev/null +++ b/src/core/utils/package-json.ts @@ -0,0 +1,23 @@ +import packageJson from "~/../package.json"; + +const version = packageJson.version; +const description = packageJson.description; +const authorName = packageJson.author.name; +const authorEmail = packageJson.author.email; +const authorWebsite = packageJson.author.url; +const license = packageJson.license; +const contributers = packageJson.contributors; +const dependencies = packageJson.dependencies; +const devDependencies = packageJson.devDependencies; + +export { + version, + description, + authorName, + authorEmail, + authorWebsite, + license, + contributers, + dependencies, + devDependencies, +}; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index a77d2b44..b57fbf7f 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -3,6 +3,18 @@ import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/respone-handler"; import { config } from "~/typings/database"; +import { + version, + authorEmail, + authorName, + authorWebsite, + contributers, + dependencies, + description, + devDependencies, + license, +} from "~/core/utils/package-json"; +import { describe } from "test"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) .get( @@ -55,4 +67,27 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) }), tags: ["Management"], }, - ); + ) + .get("/package", async ({ set }) => { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributers: contributers, + dependencies: dependencies, + devDependencies: devDependencies, + }; + return data; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error while reading package.json", + ); + } + }); diff --git a/tsconfig.json b/tsconfig.json index b95e7e02..ab566ad1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -39,7 +39,7 @@ ] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ + "resolveJsonModule": true /* Enable importing .json files. */, // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ From d1566b16f3e76e7494c438e92c427efc9f0d4494 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 18:38:04 +0100 Subject: [PATCH 146/369] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 25778667..f1be46f2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +# Deprecation Warning! +# V2 is abondend, since there whhere to many issues in the codebase! +# Please see v3 (next commit from this one onwards, all other branches which are not based on bun will be deleted!) + # DockStatAPI v2 ![Dockstat Logo](.github/DockStat.png) From 0ed8ebdfc157d342d351e91bcfc9d2403fe49481 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 18:38:26 +0100 Subject: [PATCH 147/369] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f1be46f2..4fc979cf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Deprecation Warning! -# V2 is abondend, since there whhere to many issues in the codebase! +# V2 is abondend, since there where to many issues in the codebase! # Please see v3 (next commit from this one onwards, all other branches which are not based on bun will be deleted!) # DockStatAPI v2 From 7d6fafe3ef8d6c5709b44e235d59c6e741aba0ef Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:14:51 +0100 Subject: [PATCH 148/369] Feat: Stacks, RealyController (WIP) --- .dockerignore | 17 +- .gitignore | 48 +--- .local-tests/stacks.md | 39 +++ bun.lock | 6 + package.json | 12 +- public/404.html | 15 +- src/core/database/repository.ts | 187 ++++++++++--- src/core/docker/client.ts | 17 ++ src/core/docker/relay-controller.ts | 13 + src/index.ts | 13 +- src/routes/api-config.ts | 7 +- src/routes/stacks.ts | 245 +++++++++++++++++ src/typings/database.ts | 12 +- src/typings/docker-compose.ts | 393 ++++++++++++++++++++++++++++ 14 files changed, 907 insertions(+), 117 deletions(-) create mode 100644 .local-tests/stacks.md create mode 100644 src/core/docker/relay-controller.ts create mode 100644 src/routes/stacks.ts create mode 100644 src/typings/docker-compose.ts diff --git a/.dockerignore b/.dockerignore index f965aed1..152a6192 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,15 +1,4 @@ +*.db* +stacks node_modules -Dockerfile* -docker-compose* -.dockerignore -.git -.gitignore -README.md -LICENSE -.vscode -Makefile -helm-charts -.env -.editorconfig -.idea -coverage* +*.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index 138ece65..964a9182 100644 --- a/.gitignore +++ b/.gitignore @@ -1,45 +1,3 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -*.db -*.db-journal - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env.local -.env.development.local -.env.test.local -.env.production.local - -# vercel -.vercel - -**/*.trace -**/*.zip -**/*.tar.gz -**/*.tgz -**/*.log -package-lock.json -**/*.bun +*.db* +stacks +node_modules \ No newline at end of file diff --git a/.local-tests/stacks.md b/.local-tests/stacks.md new file mode 100644 index 00000000..4d9290cb --- /dev/null +++ b/.local-tests/stacks.md @@ -0,0 +1,39 @@ +# Testing Stacks + +## Deployment + +### Values + +- compose_spec +- name +- version +- automatic_reboot_on_error +- isCustom +- image_updates +- source +- stack_prefix + +### JSON +```json +{ + "compose_spec": { + "name": "Local Test", + "services": { + "nginx": { + "container_name": "Local-test-nginx", + "image": "dockerbogo/docker-nginx-hello-world", + "ports": [ + "8081:80" + ] + } + } + }, + "name": "Local-Test", + "version": 1, + "automatic_reboot_on_error": true, + "isCustom": true, + "image_updates": true, + "source": "Local", + "stack_prefix": "" +} +``` diff --git a/bun.lock b/bun.lock index c03c8c92..f91ae698 100644 --- a/bun.lock +++ b/bun.lock @@ -7,11 +7,13 @@ "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "chalk": "^5.4.1", + "docker-compose": "^1.1.1", "dockerode": "^4.0.4", "elysia": "latest", "split2": "^4.2.0", "winston": "^3.17.0", "winston-transport": "^4.9.0", + "yaml": "^2.7.0", }, "devDependencies": { "@types/dockerode": "^3.3.34", @@ -134,6 +136,8 @@ "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "docker-compose": ["docker-compose@1.1.1", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-UkIUz0LtzuO17Ijm6SXMGtfZMs7IvbNwvuJBiBuN93PIhr/n9/sbJMqpvYFaCBGfwu1ZM4PPPDgQzeeke4lEoA=="], + "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], "dockerode": ["dockerode@4.0.4", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.0.1", "uuid": "^10.0.0" } }, "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w=="], @@ -264,6 +268,8 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], diff --git a/package.json b/package.json index d28b22ad..06509479 100644 --- a/package.json +++ b/package.json @@ -13,17 +13,23 @@ "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", "start:linux": "NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", - "build": "bun build --target bun src/index.ts --outdir ./dist" + "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", + "build": "bun build --target bun src/index.ts --outdir ./dist", + "clean": "bun run clean:win || bun run clean:lin", + "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q dockstatapi.db* && echo 'success'", + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f dockstatapi.db* && echo 'success'" }, "dependencies": { "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "chalk": "^5.4.1", + "docker-compose": "^1.1.1", "dockerode": "^4.0.4", "elysia": "latest", "split2": "^4.2.0", "winston": "^3.17.0", - "winston-transport": "^4.9.0" + "winston-transport": "^4.9.0", + "yaml": "^2.7.0" }, "devDependencies": { "@types/dockerode": "^3.3.34", @@ -36,4 +42,4 @@ "trustedDependencies": [ "protobufjs" ] -} +} \ No newline at end of file diff --git a/public/404.html b/public/404.html index a0169cf7..39b107d4 100644 --- a/public/404.html +++ b/public/404.html @@ -26,7 +26,6 @@ .logo { height: 15vh; margin-bottom: 1rem; - animation: bounce 4s infinite; } .error-code { @@ -66,27 +65,17 @@ transform: translateY(-2px); } - @keyframes bounce { - - 0%, - 100% { - transform: scale(1, 1); - } - - 50% { - transform: scale(1.1, 1.1); - } - } - @keyframes error { 0%, 100% { color: rgb(255, 255, 255); + transform: scale(1, 1); } 50% { color: rgb(255, 168, 168); + transform: scale(1.1, 1.1); } } diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index bb43497f..027f2ebc 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -1,15 +1,35 @@ import Database from "bun:sqlite"; import { logger } from "~/core/utils/logger"; -import type { DockerHost } from "~/typings/docker"; -import type { HostStats } from "~/typings/docker"; +import { relayController } from "~/core/docker/relay-controller"; +import type { DockerHost, HostStats } from "~/typings/docker"; +import type { stacks_config } from "~/typings/database"; const db = new Database("dockstatapi.db"); +db.exec("PRAGMA journal_mode = WAL;"); export const dbFunctions = { init() { const startTime = Date.now(); - logger.debug("__task__ __db__ Initializing Database ⏳") db.exec(` + CREATE TABLE IF NOT EXISTS backend_log_entries ( + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + level TEXT, + message TEXT, + file TEXT, + line NUMBER + ); + + CREATE TABLE IF NOT EXISTS stacks_config ( + name TEXT PRIMARY KEY, + version INTEGER, + custom BOOLEAN, + source TEXT, + container_count INTEGER, + stack_prefix TEXT, + automatic_reboot_on_error BOOLEAN, + image_updates BOOLEAN + ); + CREATE TABLE IF NOT EXISTS docker_hosts ( name TEXT, url TEXT, @@ -49,16 +69,11 @@ export const dbFunctions = { keep_data_for NUMBER, fetching_interval NUMBER ); - - CREATE TABLE IF NOT EXISTS backend_log_entries ( - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - level TEXT, - message TEXT, - file TEXT, - line NUMBER - ); `); + logger.info("Starting server..."); + + /* * Default values: * - Websocket polling interval 5 seconds @@ -90,6 +105,7 @@ export const dbFunctions = { ); stmt.run("Localhost", "localhost:2375", false); } + logger.debug("__task__ __db__ Initializing Database ⏳") const duration = Date.now() - startTime; logger.debug(`__task__ __db__ Initializing Database ✔️ (${duration}ms)`); }, @@ -301,6 +317,29 @@ export const dbFunctions = { return data }, + deleteOldData(days: number) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Deleting old data ⏳") + if (typeof days !== "number") { + logger.crit("Invalid parameter type for deleteOldData"); + throw new TypeError("Days parameter must be a number"); + } + + const deleteContainerStmt = db.prepare(` + DELETE FROM container_stats + WHERE timestamp < datetime('now', '-' || ? || ' days') + `); + deleteContainerStmt.run(days); + + const deleteLogsStmt = db.prepare(` + DELETE FROM backend_log_entries + WHERE timestamp < datetime('now', '-' || ? || ' days') + `); + deleteLogsStmt.run(days); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Deleting old data ✔️ (${duration}ms)`); + }, + // Stats: addContainerStats( id: string, @@ -347,29 +386,6 @@ export const dbFunctions = { return data }, - deleteOldData(days: number) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Deleting old data ⏳") - if (typeof days !== "number") { - logger.crit("Invalid parameter type for deleteOldData"); - throw new TypeError("Days parameter must be a number"); - } - - const deleteContainerStmt = db.prepare(` - DELETE FROM container_stats - WHERE timestamp < datetime('now', '-' || ? || ' days') - `); - deleteContainerStmt.run(days); - - const deleteLogsStmt = db.prepare(` - DELETE FROM backend_log_entries - WHERE timestamp < datetime('now', '-' || ? || ' days') - `); - deleteLogsStmt.run(days); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Deleting old data ✔️ (${duration}ms)`); - }, - updateHostStats(stats: HostStats) { const startTime = Date.now(); logger.debug("__task__ __db__ Update Host Stats ⏳") @@ -424,4 +440,105 @@ export const dbFunctions = { logger.debug(`__task__ __db__ Update Host stats ✔️ (${duration}ms)`); return data }, -}; + + // Stacks: + // This is the stack config which will be saved in the database, the "real" docker-compose can be found in the designated folder + addStack(stack_config: stacks_config) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Add Stack config ⏳") + + const stmt = db.prepare(` + INSERT INTO stacks_config ( + name, + version, + custom, + source, + container_count, + stack_prefix, + automatic_reboot_on_error, + image_updates + ) + VALUES(?, ?, ?, ?, ?, ?, ?, ?); + `); + const data = stmt.run( + (stack_config.name as any), // I dont fucking know bruh + stack_config.version, + stack_config.custom, + stack_config.source, + stack_config.container_count, + stack_config.stack_prefix, + stack_config.automatic_reboot_on_error, + stack_config.image_updates + ); + + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Add Stack config ✔️ (${duration}ms)`); + relayController.stackAdded(); + return data; + }, + + getStacks() { + const startTime = Date.now(); + logger.debug("__task__ __db__ Get Stack config ⏳") + + const stmt = db.prepare(` + SELECT name, version, custom, source, container_count, stack_prefix, automatic_reboot_on_error, image_updates + FROM stacks_config + ORDER BY name DESC + `); + const data = stmt.all(); + + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Get Stack config ✔️ (${duration}ms)`); + return data; + }, + + deleteStack(name: string) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Delete Stack config ⏳"); + + const stmt = db.prepare(` + DELETE FROM stacks_config + WHERE name = ?; + `); + const data = stmt.run(name); + + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Delete Stack config ✔️ (${duration}ms)`); + relayController.stackDeleted(); + return data; + }, + + updateStack(stack_config: stacks_config) { + const startTime = Date.now(); + logger.debug("__task__ __db__ Update Stack config ⏳"); + + const stmt = db.prepare(` + UPDATE stacks_config + SET + version = ?, + custom = ?, + source = ?, + container_count = ?, + stack_prefix = ?, + automatic_reboot_on_error = ?, + image_updates = ? + WHERE name = ?; + `); + const data = stmt.run( + stack_config.version, + stack_config.custom, + stack_config.source, + stack_config.container_count, + stack_config.stack_prefix, + stack_config.automatic_reboot_on_error, + stack_config.image_updates, + (stack_config.name as any) // Bruh what is this :sob: + ); + + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ Update Stack config ✔️ (${duration}ms)`); + relayController.stackUpdated(); + return data; + } +}; \ No newline at end of file diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index da174032..3d5baff4 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -18,3 +18,20 @@ export const getDockerClient = (host: DockerHost): Docker => { throw new Error("Invalid Docker host configuration"); } }; + +export const stackClient = (): Docker => { + try { + const docker = new Docker({ + socketPath: "/var/run/docker.sock" + }) + + docker.ping().catch(() => { + throw new Error("Could not ping local Docker-Socket") + }); + + return docker; + } catch (error) { + logger.error(`Could not create Docker client for "/var/run/docker.sock" - ${error as string}`) + throw new Error() + } +} \ No newline at end of file diff --git a/src/core/docker/relay-controller.ts b/src/core/docker/relay-controller.ts new file mode 100644 index 00000000..db8b6bb5 --- /dev/null +++ b/src/core/docker/relay-controller.ts @@ -0,0 +1,13 @@ +// Import any function here, when any of the specifies functions is detected, it will run said function + +export const relayController = { + stackAdded() { + + }, + stackDeleted() { + + }, + stackUpdated() { + + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 3a4521f9..a2dfb814 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,13 +7,12 @@ import { dockerRoutes } from "~/routes/docker-manager"; import { dockerStatsRoutes } from "~/routes/docker-stats"; import { backendLogs } from "~/routes/logs"; import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; +import { stackRoutes } from "./routes/stacks"; import { apiConfigRoutes } from "~/routes/api-config"; import { setSchedules } from "~/core/docker/scheduler"; import staticPlugin from "@elysiajs/static"; console.log("") -logger.info("Starting server..."); - dbFunctions.init(); const DockStatAPI = new Elysia() @@ -36,6 +35,10 @@ const DockStatAPI = new Elysia() name: "Management", description: "Various endpoints for managing DockStatAPI", }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, { name: "Utils", description: "Various utilities which might be useful", @@ -49,6 +52,7 @@ const DockStatAPI = new Elysia() .use(backendLogs) .use(dockerWebsocketRoutes) .use(apiConfigRoutes) + .use(stackRoutes) .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) .onError(({ code, set }) => { if (code === 'NOT_FOUND') { @@ -63,7 +67,7 @@ async function startServer() { try { await loadPlugins("./src/plugins"); DockStatAPI.listen(3000, ({ hostname, port }) => { - console.log("") + console.log("----- [ ############## ]") logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( `Swagger API Documentation available at http://${hostname}:${port}/swagger`, @@ -79,4 +83,5 @@ await setSchedules(); await startServer(); logger.info("Started server"); -console.log("") +console.log("----- [ ############## ]") + diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index b57fbf7f..14006ebc 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -14,7 +14,6 @@ import { devDependencies, license, } from "~/core/utils/package-json"; -import { describe } from "test"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) .get( @@ -90,4 +89,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) "Error while reading package.json", ); } - }); + }, + { + tags: ["Management"], + }, + ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts new file mode 100644 index 00000000..82cdc9d4 --- /dev/null +++ b/src/routes/stacks.ts @@ -0,0 +1,245 @@ +import { Elysia, error, t } from "elysia"; +import { responseHandler } from "~/core/utils/respone-handler"; +import { + deployStack, + stopStack, + pullStackImages, + restartStack, + getStackStatus, + startStack +} from "~/core/stacks/controller"; +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; + +export const stackRoutes = new Elysia({ prefix: "/stacks" }) + .post( + "/deploy", + async ({ set, body }) => { + try { + const isCustom = body.isCustom + ? body.isCustom + : false; + + const image_updates = body.image_updates + ? body.image_updates + : false; + + let errMsg: string = ""; + if (!body.compose_spec) { + errMsg = "compose_spec" + } + + if (!body.automatic_reboot_on_error) { + errMsg = `${errMsg} automatic_reboot_on_error` + } + + if (!body.source) { + errMsg = `${errMsg} source` + } + + if (!body.name) { + errMsg = `${errMsg} name` + } + + if (errMsg) { + errMsg = errMsg.trim(); + errMsg = `Missing values of: ${errMsg.replaceAll(" ", "; ")}` + return responseHandler.error(set, errMsg, errMsg) + } + + await deployStack( + body.compose_spec, + body.name, + body.version, + body.source, + body.automatic_reboot_on_error, + isCustom, + image_updates, + body.stack_prefix + ); + logger.info(`Deployed Stack (${body.name})`) + return responseHandler.ok( + set, + `Stack ${body.name} deployed successfully` + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error deploying stack" + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + compose_spec: t.Any(), + name: t.String(), + version: t.Number(), + automatic_reboot_on_error: t.Boolean(), + isCustom: t.Boolean(), + image_updates: t.Boolean(), + source: t.String(), + stack_prefix: t.Optional(t.String()), + }), + } + ) + .post( + "/start", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed") + } + await startStack(body.stack); + logger.info(`Started Stack (${body.stack})`) + return responseHandler.ok( + set, + `Stack ${body.stack} started successfully` + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error starting stack" + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), + } + ) + .post( + "/stop", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed") + } + await stopStack(body.stack); + logger.info(`Stopped Stack (${body.stack})`) + return responseHandler.ok( + set, + `Stack ${body.stack} stopped successfully` + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error stopping stack" + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), + } + ) + .post( + "/restart", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed") + } + await restartStack(body.stack); + logger.info(`Restarted Stack (${body.stack})`) + return responseHandler.ok( + set, + `Stack ${body.stack} restarted successfully` + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error restarting stack" + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), + } + ) + .post( + "/pull-images", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed") + } + await pullStackImages(body.stack); + logger.info(`Pulled Stack images (${body.stack})`) + return responseHandler.ok( + set, + `Images for stack ${body.stack} pulled successfully` + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error pulling images" + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), + } + ) + .get( + "/status", + async ({ set, query }) => { + try { + if (!query.stack_name) { + throw new Error("Stack needed") + } + logger.debug(query.stack_name) + const status = await getStackStatus(query.stack_name); + const res = responseHandler.ok( + set, + `Stack ${query.stack_name} status retrieved successfully` + ); + logger.info("Fetched Stack status") + return { ...res, status: status }; + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error getting stack status" + ); + } + }, + { + detail: { tags: ["Stacks"] }, + query: t.Object({ + stack_name: t.Any(), + }), + } + ) + .get("/", async ({ set }) => { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks") + return stacks; + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error getting stacks" + ); + } + }, + { + detail: { tags: ["Stacks"] }, + } + ); diff --git a/src/typings/database.ts b/src/typings/database.ts index 425fa460..7f9afe61 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -11,5 +11,15 @@ interface config { keep_data_for: number; fetching_interval: number; } +interface stacks_config { + name: string; + version: number; + custom: Boolean; + source: string; + container_count: number; + stack_prefix: string; + automatic_reboot_on_error: Boolean; + image_updates: Boolean; +} -export type { backend_log_entries, config }; +export type { backend_log_entries, config, stacks_config }; diff --git a/src/typings/docker-compose.ts b/src/typings/docker-compose.ts new file mode 100644 index 00000000..a554c21c --- /dev/null +++ b/src/typings/docker-compose.ts @@ -0,0 +1,393 @@ +export interface Stack { + compose_spec: ComposeSpec; + name: string + version: number; + source: string; +} + +export interface ComposeSpec { + version?: string; + name?: string; + include?: Include[]; + services?: { [key: string]: Service }; + networks?: { [key: string]: Network }; + volumes?: { [key: string]: Volume }; + secrets?: { [key: string]: Secret }; + configs?: { [key: string]: Config }; + [key: `x-${string}`]: any; +} + +type Include = string | { path: string | string[]; env_file?: string | string[]; project_directory?: string }; + +interface Service { + develop?: Development | null; + deploy?: Deployment | null; + annotations?: ListOrDict; + attach?: boolean | string; + build?: string | { + context?: string; + dockerfile?: string; + dockerfile_inline?: string; + entitlements?: string[]; + args?: ListOrDict; + ssh?: ListOrDict; + labels?: ListOrDict; + cache_from?: string[]; + cache_to?: string[]; + no_cache?: boolean | string; + additional_contexts?: ListOrDict; + network?: string; + pull?: boolean | string; + target?: string; + shm_size?: number | string; + extra_hosts?: ExtraHosts; + isolation?: string; + privileged?: boolean | string; + secrets?: ServiceConfigOrSecret[]; + tags?: string[]; + ulimits?: Ulimits; + platforms?: string[]; + [key: `x-${string}`]: any; + }; + blkio_config?: { + device_read_bps?: BlkioLimit[]; + device_read_iops?: BlkioLimit[]; + device_write_bps?: BlkioLimit[]; + device_write_iops?: BlkioLimit[]; + weight?: number | string; + weight_device?: BlkioWeight[]; + }; + cap_add?: string[]; + cap_drop?: string[]; + cgroup?: 'host' | 'private'; + cgroup_parent?: string; + command?: Command; + configs?: ServiceConfigOrSecret[]; + container_name?: string; + cpu_count?: string | number; + cpu_percent?: string | number; + cpu_shares?: number | string; + cpu_quota?: number | string; + cpu_period?: number | string; + cpu_rt_period?: number | string; + cpu_rt_runtime?: number | string; + cpus?: number | string; + cpuset?: string; + credential_spec?: { + config?: string; + file?: string; + registry?: string; + [key: `x-${string}`]: any; + }; + depends_on?: string[] | { + [service: string]: { + condition: 'service_started' | 'service_healthy' | 'service_completed_successfully'; + restart?: boolean | string; + required?: boolean; + [key: `x-${string}`]: any; + } + }; + device_cgroup_rules?: string[]; + devices?: (string | { + source: string; + target?: string; + permissions?: string; + [key: `x-${string}`]: any; + })[]; + dns?: StringOrList; + dns_opt?: string[]; + dns_search?: StringOrList; + domainname?: string; + entrypoint?: Command; + env_file?: EnvFile; + label_file?: string | string[]; + environment?: ListOrDict; + expose?: (string | number)[]; + extends?: string | { service: string; file?: string }; + external_links?: string[]; + extra_hosts?: ExtraHosts; + gpus?: 'all' | Array<{ + capabilities?: string[]; + count?: string | number; + device_ids?: string[]; + driver?: string; + options?: ListOrDict; + [key: `x-${string}`]: any; + }>; + group_add?: (string | number)[]; + healthcheck?: Healthcheck; + hostname?: string; + image?: string; + init?: boolean | string; + ipc?: string; + isolation?: string; + labels?: ListOrDict; + links?: string[]; + logging?: { + driver?: string; + options?: { [key: string]: string | number | null }; + [key: `x-${string}`]: any; + }; + mac_address?: string; + mem_limit?: number | string; + mem_reservation?: string | number; + mem_swappiness?: number | string; + memswap_limit?: number | string; + network_mode?: string; + networks?: string[] | { + [network: string]: { + aliases?: string[]; + ipv4_address?: string; + ipv6_address?: string; + link_local_ips?: string[]; + mac_address?: string; + driver_opts?: { [key: string]: string | number }; + priority?: number; + [key: `x-${string}`]: any; + } | null; + }; + oom_kill_disable?: boolean | string; + oom_score_adj?: string | number; + pid?: string | null; + pids_limit?: number | string; + platform?: string; + ports?: (number | string | { + name?: string; + mode?: string; + host_ip?: string; + target?: number | string; + published?: string | number; + protocol?: string; + app_protocol?: string; + [key: `x-${string}`]: any; + })[]; + post_start?: ServiceHook[]; + pre_stop?: ServiceHook[]; + privileged?: boolean | string; + profiles?: string[]; + pull_policy?: 'always' | 'never' | 'if_not_present' | 'build' | 'missing'; + read_only?: boolean | string; + restart?: string; + runtime?: string; + scale?: number | string; + security_opt?: string[]; + shm_size?: number | string; + secrets?: ServiceConfigOrSecret[]; + sysctls?: ListOrDict; + stdin_open?: boolean | string; + stop_grace_period?: string; + stop_signal?: string; + storage_opt?: object; + tmpfs?: StringOrList; + tty?: boolean | string; + ulimits?: Ulimits; + user?: string; + uts?: string; + userns_mode?: string; + volumes?: (string | { + type: string; + source?: string; + target?: string; + read_only?: boolean | string; + consistency?: string; + bind?: { + propagation?: string; + create_host_path?: boolean | string; + recursive?: 'enabled' | 'disabled' | 'writable' | 'readonly'; + selinux?: 'z' | 'Z'; + [key: `x-${string}`]: any; + }; + volume?: { + nocopy?: boolean | string; + subpath?: string; + [key: `x-${string}`]: any; + }; + tmpfs?: { + size?: number | string; + mode?: number | string; + [key: `x-${string}`]: any; + }; + [key: `x-${string}`]: any; + })[]; + volumes_from?: string[]; + working_dir?: string; + [key: `x-${string}`]: any; +} + +interface Healthcheck { + disable?: boolean | string; + interval?: string; + retries?: number | string; + test?: string | string[]; + timeout?: string; + start_period?: string; + start_interval?: string; + [key: `x-${string}`]: any; +} + +interface Development { + watch?: Array<{ + path: string; + action: 'rebuild' | 'sync' | 'restart' | 'sync+restart' | 'sync+exec'; + ignore?: string[]; + target?: string; + exec?: ServiceHook; + [key: `x-${string}`]: any; + }>; + [key: `x-${string}`]: any; +} + +interface Deployment { + mode?: string; + endpoint_mode?: string; + replicas?: number | string; + labels?: ListOrDict; + rollback_config?: { + parallelism?: number | string; + delay?: string; + failure_action?: string; + monitor?: string; + max_failure_ratio?: number | string; + order?: 'start-first' | 'stop-first'; + [key: `x-${string}`]: any; + }; + update_config?: { + parallelism?: number | string; + delay?: string; + failure_action?: string; + monitor?: string; + max_failure_ratio?: number | string; + order?: 'start-first' | 'stop-first'; + [key: `x-${string}`]: any; + }; + resources?: { + limits?: { + cpus?: number | string; + memory?: string; + pids?: number | string; + [key: `x-${string}`]: any; + }; + reservations?: { + cpus?: number | string; + memory?: string; + generic_resources?: Array<{ + discrete_resource_spec?: { + kind?: string; + value?: number | string; + [key: `x-${string}`]: any; + }; + [key: `x-${string}`]: any; + }>; + devices?: Array<{ + capabilities?: string[]; + count?: string | number; + device_ids?: string[]; + driver?: string; + options?: ListOrDict; + [key: `x-${string}`]: any; + }>; + [key: `x-${string}`]: any; + }; + [key: `x-${string}`]: any; + }; + restart_policy?: { + condition?: string; + delay?: string; + max_attempts?: number | string; + window?: string; + [key: `x-${string}`]: any; + }; + placement?: { + constraints?: string[]; + preferences?: Array<{ + spread?: string; + [key: `x-${string}`]: any; + }>; + max_replicas_per_node?: number | string; + [key: `x-${string}`]: any; + }; + [key: `x-${string}`]: any; +} + +type Command = string | string[] | null; +type EnvFile = string | Array; +type StringOrList = string | string[]; +type ListOrDict = { [key: string]: string | number | boolean | null } | string[]; +type ExtraHosts = { [host: string]: string | string[] } | string[]; +interface BlkioLimit { path: string; rate: number | string; } +interface BlkioWeight { path: string; weight: number | string; } +type ServiceConfigOrSecret = string | { + source: string; + target?: string; + uid?: string; + gid?: string; + mode?: number | string; + [key: `x-${string}`]: any; +}; +type Ulimits = { [key: string]: number | string | { hard: number | string; soft: number | string } }; + +interface ServiceHook { + command?: Command; + user?: string; + privileged?: boolean | string; + working_dir?: string; + environment?: ListOrDict; + [key: `x-${string}`]: any; +} + +interface Network { + name?: string; + driver?: string; + driver_opts?: { [key: string]: string | number }; + ipam?: { + driver?: string; + config?: Array<{ + subnet?: string; + ip_range?: string; + gateway?: string; + aux_addresses?: { [key: string]: string }; + [key: `x-${string}`]: any; + }>; + options?: { [key: string]: string }; + [key: `x-${string}`]: any; + }; + external?: boolean | string | { name?: string;[key: `x-${string}`]: any }; + internal?: boolean | string; + enable_ipv4?: boolean | string; + enable_ipv6?: boolean | string; + attachable?: boolean | string; + labels?: ListOrDict; + [key: `x-${string}`]: any; +} + +interface Volume { + name?: string; + driver?: string; + driver_opts?: { [key: string]: string | number }; + external?: boolean | string | { name?: string;[key: `x-${string}`]: any }; + labels?: ListOrDict; + [key: `x-${string}`]: any; +} + +interface Secret { + name?: string; + environment?: string; + file?: string; + external?: boolean | string | { name?: string;[key: string]: any }; + labels?: ListOrDict; + driver?: string; + driver_opts?: { [key: string]: string | number }; + template_driver?: string; + [key: `x-${string}`]: any; +} + +interface Config { + name?: string; + content?: string; + environment?: string; + file?: string; + external?: boolean | string | { name?: string;[key: string]: any }; + labels?: ListOrDict; + template_driver?: string; + [key: `x-${string}`]: any; +} \ No newline at end of file From d2cc5016a17d84f5bc766638af3b88330f06ec36 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:16:06 +0100 Subject: [PATCH 149/369] Fix: ignore adjustments --- .dockerignore | 4 +- .gitignore | 4 +- src/core/stacks/controller.ts | 135 ++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 src/core/stacks/controller.ts diff --git a/.dockerignore b/.dockerignore index 152a6192..1e140910 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,4 @@ *.db* -stacks -node_modules +/stacks +/node_modules *.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index 964a9182..aa426ba1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ *.db* -stacks -node_modules \ No newline at end of file +/stacks +/node_modules \ No newline at end of file diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts new file mode 100644 index 00000000..133b8673 --- /dev/null +++ b/src/core/stacks/controller.ts @@ -0,0 +1,135 @@ +import { dbFunctions } from "../database/repository"; +import YAML from "yaml"; +import { logger } from "../utils/logger"; +import DockerCompose from "docker-compose"; +import type { Stack, ComposeSpec } from "~/typings/docker-compose"; +import type { stacks_config } from "~/typings/database"; + +async function getStackPath(stack: Stack): Promise { + const stackName = stack.name.trim().replace(/\s+/g, "_"); + return `stacks/${stackName}`; +} + +async function createStackYAML(compose_spec: Stack): Promise { + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { createPath: true }); +} + +export async function deployStack( + stack: ComposeSpec, + name: string, + version: number, + source: string, + automatic_reboot_on_error: boolean, + isCustom: boolean, + image_updates: boolean, + stack_prefix?: string +): Promise { + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`) + + const serviceCount = stack.services + ? Object.keys(stack.services).length + : 0; + + const resolvedPrefix = stack_prefix ?? ""; + + const stack_config: stacks_config = { + name: name, + version: version, + source, + stack_prefix: resolvedPrefix, + automatic_reboot_on_error, + container_count: serviceCount, + custom: isCustom, + image_updates, + }; + + if (!stack.name) { + logger.debug(`${JSON.stringify(stack)}`) + throw new Error("Stack name needed") + } + + dbFunctions.addStack(stack_config); + + const stackYaml: Stack = { + name: name, + source: source, + version: version, + compose_spec: stack, + } + await createStackYAML(stackYaml); + const stackPath = await getStackPath(stackYaml); + await DockerCompose.upAll({ cwd: stackPath }); + } catch (error: any) { + throw new Error(`Error while deploying Stack: ${error.message || error}`); + } +} + +export async function stopStack(stack_name: string): Promise { + try { + const stack = { + name: stack_name + } + const stackPath = await getStackPath(stack as Stack); + await DockerCompose.downAll({ cwd: stackPath }); + } catch (error: any) { + throw new Error(`Error while stopping stack "${stack_name}": ${error.message || error}`); + } +} + +export async function startStack(stack_name: string): Promise { + try { + const stack = { + name: stack_name + } + const stackPath = await getStackPath(stack as Stack); + await DockerCompose.upAll({ cwd: stackPath }); + } catch (error: any) { + throw new Error(`Error while starting stack "${stack_name}": ${error.message || error}`); + } +} + +export async function pullStackImages(stack_name: string): Promise { + try { + const stack = { + name: stack_name + } + const stackPath = await getStackPath(stack as Stack); + await DockerCompose.pullAll({ cwd: stackPath }); + } catch (error: any) { + throw new Error(`Error while pulling images for stack "${stack_name}": ${error.message || error}`); + } +} + +export async function restartStack(stack_name: string): Promise { + try { + const stack = { + name: stack_name + } + const stackPath = await getStackPath(stack as Stack); + await DockerCompose.restartAll({ cwd: stackPath }); + } catch (error: any) { + throw new Error(`Error while restarting stack "${stack_name}": ${error.message || error}`); + } +} + +export async function getStackStatus(stack_name: string): Promise { + try { + logger.debug("Retrieving status for Stack:", stack_name); + const stackYaml = { name: stack_name }; + const stackPath = await getStackPath(stackYaml as Stack); + const rawStatus = await DockerCompose.ps({ cwd: stackPath }); + + const transformedStatus = rawStatus.data.services.reduce((acc: any, service: any) => { + acc[(service.name)] = service.state; + return acc; + }, {}); + + return transformedStatus; + } catch (error: any) { + throw new Error(`Error while retrieving status for stack "${stack_name}": ${error.message || error}`); + } +} + From 1b6b04adfd22f5915ee4cdceae3006bac31d31b1 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:20:13 +0100 Subject: [PATCH 150/369] Update README.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9784f980..ad1371b2 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ The DockStat API provides the following endpoints: - `DELETE /logs`: Clear all backend logs. - `DELETE /logs/:level`: Clear logs by log level. -### Webocket +### Websocket - `WS(S) /docker/stats`: Retrieve the current API configuration. ## API From a07499d70cc49c43ac599773a2cca85ab0126388 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:21:57 +0100 Subject: [PATCH 151/369] Inline variable that is immediately returned (inline-immediately-returned-variable) Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/core/stacks/controller.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 133b8673..2ff5edb9 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -122,12 +122,11 @@ export async function getStackStatus(stack_name: string): Promise { const stackPath = await getStackPath(stackYaml as Stack); const rawStatus = await DockerCompose.ps({ cwd: stackPath }); - const transformedStatus = rawStatus.data.services.reduce((acc: any, service: any) => { - acc[(service.name)] = service.state; - return acc; - }, {}); + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[(service.name)] = service.state; + return acc; + }, {}); - return transformedStatus; } catch (error: any) { throw new Error(`Error while retrieving status for stack "${stack_name}": ${error.message || error}`); } From 26ebd1e8d330d381696930e62e3754ecb74b8f2f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:22:34 +0100 Subject: [PATCH 152/369] Avoid unneeded ternary statements (simplify-ternary) Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/routes/stacks.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 82cdc9d4..3108e0cd 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -20,9 +20,8 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) ? body.isCustom : false; - const image_updates = body.image_updates - ? body.image_updates - : false; + const image_updates = body.image_updates || false; + let errMsg: string = ""; if (!body.compose_spec) { From 35f70f831e3733e09662143292bd118d9f217f69 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:22:44 +0100 Subject: [PATCH 153/369] Avoid unneeded ternary statements (simplify-ternary) Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/routes/stacks.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 3108e0cd..f113d14c 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -16,9 +16,8 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) "/deploy", async ({ set, body }) => { try { - const isCustom = body.isCustom - ? body.isCustom - : false; + const isCustom = body.isCustom || false; + const image_updates = body.image_updates || false; From a827c0ab844f15f1188f787ee003412e86d08a10 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:33:07 +0100 Subject: [PATCH 154/369] Inline variable that is immediately returned (inline-immediately-returned-variable) Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/routes/api-config.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 14006ebc..5be4dfb6 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -70,18 +70,18 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) .get("/package", async ({ set }) => { try { logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributers: contributers, - dependencies: dependencies, - devDependencies: devDependencies, - }; - return data; + return { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributers: contributers, + dependencies: dependencies, + devDependencies: devDependencies, + }; + } catch (error) { return responseHandler.error( set, From 4155566dac5af47a5dedf63af02fac45bb13c647 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:40:03 +0100 Subject: [PATCH 155/369] Fix: Code Quality --- src/core/utils/package-json.ts | 9 ++------- src/routes/docker-websocket.ts | 21 ++++++++++++++++----- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/core/utils/package-json.ts b/src/core/utils/package-json.ts index 5b84757d..3872d561 100644 --- a/src/core/utils/package-json.ts +++ b/src/core/utils/package-json.ts @@ -1,14 +1,9 @@ import packageJson from "~/../package.json"; +const { version, description, license, contributors, dependencies, devDependencies } = packageJson; -const version = packageJson.version; -const description = packageJson.description; const authorName = packageJson.author.name; const authorEmail = packageJson.author.email; const authorWebsite = packageJson.author.url; -const license = packageJson.license; -const contributers = packageJson.contributors; -const dependencies = packageJson.dependencies; -const devDependencies = packageJson.devDependencies; export { version, @@ -17,7 +12,7 @@ export { authorEmail, authorWebsite, license, - contributers, + contributors, dependencies, devDependencies, }; diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index f1e6e684..e0406459 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -62,7 +62,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( }, 30000); for (const host of hosts) { - if (!(socket as any).isOpen) break; + if (!(socket as any).isOpen) { + break + }; logger.debug(`Processing host: ${host.name}`); @@ -77,7 +79,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( ); for (const containerInfo of containers) { - if (!(socket as any).isOpen) break; + if (!(socket as any).isOpen) { + break + }; logger.debug( `Processing container ${containerInfo.Id} on host ${host.name}`, @@ -109,8 +113,13 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( statsStream .pipe(splitStream) .on("data", (line: string) => { - if (socket.readyState !== 1) return; // 1 = OPEN state - if (!line) return; + // 1 = OPEN state + if (socket.readyState !== 1) { + return + }; + if (!line) { + return + }; try { const stats = JSON.parse(line); const cpuUsage = calculateCpuPercent(stats); @@ -187,7 +196,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( }, message(socket, message) { - if (message === "pong") return; + if (message === "pong") { + return + }; }, close(socket, code, reason) { From 1ba080d1aba00701b7cea64c22417e7e425fa166 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:44:47 +0100 Subject: [PATCH 156/369] Fix: Adjust Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad1371b2..ed73de8c 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ The DockStat API exposes the following endpoints: ## License This project is licensed under the CC BY-NC 4.0 License. -[cc-by-nc-image]: https://licensebuttons.net/l/by-nc/4.0/88x31.png +[cc-by-nc-image]https://licensebuttons.net/l/by-nc/4.0/88x31.png ## Testing From 3b4696a9b212ee682b129e073716f60794761172 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:45:21 +0100 Subject: [PATCH 157/369] Fix: Adjust Rreadme (forgot how markdown works) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed73de8c..eae97c47 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ The DockStat API exposes the following endpoints: ## License This project is licensed under the CC BY-NC 4.0 License. -[cc-by-nc-image]https://licensebuttons.net/l/by-nc/4.0/88x31.png +[cc-by-nc-image](https://licensebuttons.net/l/by-nc/4.0/88x31.png) ## Testing From 03c872d9cdadae274bc2f58a3cc4d4d00ce012e0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 22:45:48 +0100 Subject: [PATCH 158/369] Fix: Adjust Readme (this is getting sill) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eae97c47..3ea81873 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ The DockStat API exposes the following endpoints: ## License This project is licensed under the CC BY-NC 4.0 License. -[cc-by-nc-image](https://licensebuttons.net/l/by-nc/4.0/88x31.png) +![cc-by-nc-image](https://licensebuttons.net/l/by-nc/4.0/88x31.png) ## Testing From 745856e6b0e8f3ac5872f8a7ec88f8e85394e53b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 23:01:32 +0100 Subject: [PATCH 159/369] Fix: Fixes #38 --- src/core/plugins/loader.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts index 26d00e5b..db77bedd 100644 --- a/src/core/plugins/loader.ts +++ b/src/core/plugins/loader.ts @@ -16,7 +16,9 @@ export async function loadPlugins(pluginDir: string) { const files = fs.readdirSync(pluginPath); for (const file of files) { - if (!file.endsWith(".plugin.ts")) continue; + if (!file.endsWith(".plugin.ts")) { + continue + }; const absolutePath = path.join(pluginPath, file); try { From ba507e2d45338bf7c21f860a8d6c29b42219a273 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 11 Mar 2025 23:04:12 +0100 Subject: [PATCH 160/369] Fix: Fixes #37 --- src/routes/api-config.ts | 22 +++++++++++----------- src/routes/stacks.ts | 20 ++++++++------------ 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 5be4dfb6..e6185517 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -8,7 +8,7 @@ import { authorEmail, authorName, authorWebsite, - contributers, + contributors, dependencies, description, devDependencies, @@ -71,16 +71,16 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) try { logger.debug("Fetching package.json"); return { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributers: contributers, - dependencies: dependencies, - devDependencies: devDependencies, - }; + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; } catch (error) { return responseHandler.error( diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index f113d14c..600dec55 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -22,27 +22,23 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) const image_updates = body.image_updates || false; - let errMsg: string = ""; + let missingParams: string[] = []; if (!body.compose_spec) { - errMsg = "compose_spec" + missingParams.push("compose_spec"); } - if (!body.automatic_reboot_on_error) { - errMsg = `${errMsg} automatic_reboot_on_error` + missingParams.push("automatic_reboot_on_error"); } - if (!body.source) { - errMsg = `${errMsg} source` + missingParams.push("source"); } - if (!body.name) { - errMsg = `${errMsg} name` + missingParams.push("name"); } - if (errMsg) { - errMsg = errMsg.trim(); - errMsg = `Missing values of: ${errMsg.replaceAll(" ", "; ")}` - return responseHandler.error(set, errMsg, errMsg) + if (missingParams.length > 0) { + const errMsg = `Missing values of: ${missingParams.join("; ")}`; + return responseHandler.error(set, errMsg, errMsg); } await deployStack( From cf33ba6b70dee5dd9d660d1233f6baa3a408020a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 12 Mar 2025 19:19:06 +0100 Subject: [PATCH 161/369] Fix: Fixes #34 --- src/core/database/repository.ts | 5 +++-- src/typings/database.ts | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 027f2ebc..899f7b7f 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -460,8 +460,9 @@ export const dbFunctions = { ) VALUES(?, ?, ?, ?, ?, ?, ?, ?); `); + const data = stmt.run( - (stack_config.name as any), // I dont fucking know bruh + stack_config.name, stack_config.version, stack_config.custom, stack_config.source, @@ -533,7 +534,7 @@ export const dbFunctions = { stack_config.stack_prefix, stack_config.automatic_reboot_on_error, stack_config.image_updates, - (stack_config.name as any) // Bruh what is this :sob: + stack_config.name ); const duration = Date.now() - startTime; diff --git a/src/typings/database.ts b/src/typings/database.ts index 7f9afe61..3d95b353 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -14,12 +14,12 @@ interface config { interface stacks_config { name: string; version: number; - custom: Boolean; + custom: boolean; source: string; container_count: number; stack_prefix: string; - automatic_reboot_on_error: Boolean; - image_updates: Boolean; + automatic_reboot_on_error: boolean; + image_updates: boolean; } export type { backend_log_entries, config, stacks_config }; From 2e1d948327248b3e2a95bd33fac9efa3a1acbc7e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 12 Mar 2025 19:57:44 +0100 Subject: [PATCH 162/369] Fix: Fixes #39 --- src/core/database/helper.ts | 17 + src/core/database/repository.ts | 676 ++++++++++++++++---------------- src/core/utils/logger.ts | 27 +- 3 files changed, 379 insertions(+), 341 deletions(-) create mode 100644 src/core/database/helper.ts diff --git a/src/core/database/helper.ts b/src/core/database/helper.ts new file mode 100644 index 00000000..3bea0446 --- /dev/null +++ b/src/core/database/helper.ts @@ -0,0 +1,17 @@ +import { logger } from "../utils/logger"; + +export function executeDbOperation( + label: string, + operation: () => T, + validate?: () => void +): T { + const startTime = Date.now(); + logger.debug(`__task__ __db__ ${label} �3`); + if (validate) { + validate(); + } + const result = operation(); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ ${label} �4f (${duration}ms)`); + return result; +} \ No newline at end of file diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 899f7b7f..56543cf2 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -1,3 +1,4 @@ +import { executeDbOperation } from "./helper"; import Database from "bun:sqlite"; import { logger } from "~/core/utils/logger"; import { relayController } from "~/core/docker/relay-controller"; @@ -111,39 +112,42 @@ export const dbFunctions = { }, addDockerHost(hostId: string, url: string, secure: boolean) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Adding Docker Host ⏳") - if ( - typeof hostId !== "string" || - typeof url !== "string" || - typeof secure !== "boolean" - ) { - logger.crit("Invalid parameter types for addDockerHost"); - throw new TypeError("Invalid parameter types for addDockerHost"); - } - - const stmt = db.prepare(` + return executeDbOperation( + "Add Docker Host", + () => { + const stmt = db.prepare(` INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) `); - const data = stmt.run(hostId, url, secure); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Adding Docker Host ✔️ (${duration}ms)`); - return data + return stmt.run(hostId, url, secure); + }, + () => { + if ( + typeof hostId !== "string" || + typeof url !== "string" || + typeof secure !== "boolean" + ) { + logger.error("Invalid parameter types for addDockerHost"); + throw new TypeError("Invalid parameter types for addDockerHost"); + } + } + ); }, getDockerHosts(): DockerHost[] { - const startTime = Date.now(); - logger.debug("__task__ __db__ Getting Docker Host ⏳") - const stmt = db.prepare(` - SELECT name, url, secure - FROM docker_hosts - ORDER BY name DESC - `); - const data = stmt.all(); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Getting Docker Host ✔️ (${duration}ms)`); - return data as DockerHost[]; + return executeDbOperation( + "Get Docker Hosts", + () => { + const stmt = db.prepare(` + SELECT name, url, secure + FROM docker_hosts + ORDER BY name DESC + `); + const data = stmt.all(); + return data as DockerHost[]; + }, + () => { } + ); }, addLogEntry: ( @@ -170,177 +174,192 @@ export const dbFunctions = { }, getAllLogs() { - const startTime = Date.now(); - logger.debug("__task__ __db__ Getting all Logs ⏳") - const stmt = db.prepare(` + return executeDbOperation( + "Get All Logs", + () => { + const stmt = db.prepare(` SELECT timestamp, level, message, file, line FROM backend_log_entries ORDER BY timestamp DESC `); - const data = stmt.all(); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Getting all Logs ✔️ (${duration}ms)`); - return data + const data = stmt.all(); + return data; + }, + () => { } + ); }, getLogsByLevel(level: string) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Getting level-logs ⏳") - if (typeof level !== "string") { - logger.crit("Level parameter must be a string"); - throw new TypeError("Level parameter must be a string"); - } - - const stmt = db.prepare(` + return executeDbOperation( + "Get Logs By Level", + () => { + const stmt = db.prepare(` SELECT timestamp, level, message, file, line FROM backend_log_entries WHERE level = ? ORDER BY timestamp DESC `); - const data = stmt.all(level); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Getting level-logs ✔️ (${duration}ms)`); - return data + const data = stmt.all(level); + return data; + }, + () => { + if (typeof level !== "string") { + logger.error("Level parameter must be a string"); + throw new TypeError("Level parameter must be a string"); + } + } + ); }, updateDockerHost(name: string, url: string, secure: boolean) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Updating Docker Host ⏳") - if ( - typeof name !== "string" || - typeof url !== "string" || - typeof secure !== "boolean" - ) { - logger.crit("Invalid parameter types for updateDockerHost"); - throw new TypeError("Invalid parameter types for updateDockerHost"); - } - - const stmt = db.prepare(` - UPDATE docker_hosts - SET url = ?, secure = ? - WHERE name = ? - `); - const data = stmt.run(url, secure, name); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Updating Docker Host ✔️ (${duration}ms)`); - return data + return executeDbOperation( + "Update Docker Host", + () => { + const stmt = db.prepare(` + UPDATE docker_hosts + SET url = ?, secure = ? + WHERE name = ? + `); + const data = stmt.run(url, secure, name); + return data; + }, + () => { + if ( + typeof name !== "string" || + typeof url !== "string" || + typeof secure !== "boolean" + ) { + logger.error("Invalid parameter types for updateDockerHost"); + throw new TypeError("Invalid parameter types for updateDockerHost"); + } + } + ); }, deleteDockerHost(name: string) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Deleting Docker Host ⏳") - if (typeof name !== "string") { - logger.crit("Invalid parameter type for deleteDockerHost"); - throw new TypeError("Name parameter must be a string"); - } - - const stmt = db.prepare(` - DELETE FROM docker_hosts - WHERE name = ? - `); - const data = stmt.run(name); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Deleting Docker Host ✔️ (${duration}ms)`); - return data + return executeDbOperation( + "Delete Docker Host", + () => { + const stmt = db.prepare(` + DELETE FROM docker_hosts + WHERE name = ? + `); + const data = stmt.run(name); + return data; + }, + () => { + if (typeof name !== "string") { + logger.error("Invalid parameter type for deleteDockerHost"); + throw new TypeError("Name parameter must be a string"); + } + } + ); }, clearAllLogs() { - const startTime = Date.now(); - logger.debug("__task__ __db__ Clearing all Logs ⏳") - const stmt = db.prepare(` - DELETE FROM backend_log_entries - `); - const data = stmt.run(); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Clearing all Logs ✔️ (${duration}ms)`); - return data + return executeDbOperation( + "Clear All Logs", + () => { + const stmt = db.prepare(` + DELETE FROM backend_log_entries + `); + const data = stmt.run(); + return data; + }, + () => { } + ); }, clearLogsByLevel(level: string) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Clearing all logs by level ⏳") - if (typeof level !== "string") { - logger.crit("Invalid parameter type for clearLogsByLevel"); - throw new TypeError("Level parameter must be a string"); - } - - const stmt = db.prepare(` - DELETE FROM backend_log_entries - WHERE level = ? - `); - const data = stmt.run(level); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Clearing all logs by level ✔️ (${duration}ms)`); - return data + return executeDbOperation( + "Clear Logs By Level", + () => { + const stmt = db.prepare(` + DELETE FROM backend_log_entries + WHERE level = ? + `); + const data = stmt.run(level); + return data; + }, + () => { + if (typeof level !== "string") { + logger.error("Invalid parameter type for clearLogsByLevel"); + throw new TypeError("Level parameter must be a string"); + } + } + ); }, updateConfig( polling_rate: number, fetching_interval: number, - keep_data_for: number, + keep_data_for: number ) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Updating config ⏳") - if ( - typeof polling_rate !== "number" || - typeof fetching_interval !== "number" || - typeof keep_data_for !== "number" - ) { - logger.crit("Invalid parameter types for updateConfig"); - throw new TypeError("Invalid parameter types for updateConfig"); - } - - const stmt = db.prepare(` - UPDATE config - SET polling_rate = ?, - fetching_interval = ?, - keep_data_for = ? - `); - - const data = stmt.run(polling_rate, fetching_interval, keep_data_for); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Updating config ✔️ (${duration}ms)`); - return data + return executeDbOperation( + "Update Config", + () => { + const stmt = db.prepare(` + UPDATE config + SET polling_rate = ?, + fetching_interval = ?, + keep_data_for = ? + `); + const data = stmt.run(polling_rate, fetching_interval, keep_data_for); + return data; + }, + () => { + if ( + typeof polling_rate !== "number" || + typeof fetching_interval !== "number" || + typeof keep_data_for !== "number" + ) { + logger.error("Invalid parameter types for updateConfig"); + throw new TypeError("Invalid parameter types for updateConfig"); + } + } + ); }, getConfig() { - const startTime = Date.now(); - logger.debug("__task__ __db__ Getting config ⏳") - const stmt = db.prepare(` - SELECT polling_rate, keep_data_for, fetching_interval - FROM config - `); - - const data = stmt.all(); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Getting config ✔️ (${duration}ms)`); - return data + return executeDbOperation( + "Get Config", + () => { + const stmt = db.prepare(` + SELECT polling_rate, keep_data_for, fetching_interval + FROM config + `); + const data = stmt.all(); + return data; + }, + () => { } + ); }, deleteOldData(days: number) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Deleting old data ⏳") - if (typeof days !== "number") { - logger.crit("Invalid parameter type for deleteOldData"); - throw new TypeError("Days parameter must be a number"); - } - - const deleteContainerStmt = db.prepare(` - DELETE FROM container_stats - WHERE timestamp < datetime('now', '-' || ? || ' days') - `); - deleteContainerStmt.run(days); + return executeDbOperation( + "Delete Old Data", + () => { + const deleteContainerStmt = db.prepare(` + DELETE FROM container_stats + WHERE timestamp < datetime('now', '-' || ? || ' days') + `); + deleteContainerStmt.run(days); - const deleteLogsStmt = db.prepare(` - DELETE FROM backend_log_entries - WHERE timestamp < datetime('now', '-' || ? || ' days') - `); - deleteLogsStmt.run(days); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Deleting old data ✔️ (${duration}ms)`); + const deleteLogsStmt = db.prepare(` + DELETE FROM backend_log_entries + WHERE timestamp < datetime('now', '-' || ? || ' days') + `); + deleteLogsStmt.run(days); + }, + () => { + if (typeof days !== "number") { + logger.error("Invalid parameter type for deleteOldData"); + throw new TypeError("Days parameter must be a number"); + } + } + ); }, - // Stats: addContainerStats( id: string, hostId: string, @@ -349,197 +368,198 @@ export const dbFunctions = { status: string, state: string, cpu_usage: number, - memory_usage: number, + memory_usage: number ) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Adding container statistics ⏳") - if ( - typeof id !== "string" || - typeof hostId !== "string" || - typeof name !== "string" || - typeof image !== "string" || - typeof status !== "string" || - typeof state !== "string" || - typeof cpu_usage !== "number" || - typeof memory_usage !== "number" - ) { - logger.crit("Invalid parameter types for addContainerStats"); - throw new TypeError("Invalid parameter types for addContainerStats"); - } - - const stmt = db.prepare(` - INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `); - const data = stmt.run( - id, - hostId, - name, - image, - status, - state, - cpu_usage, - memory_usage, + return executeDbOperation( + "Add Container Stats", + () => { + const stmt = db.prepare(` + INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + const data = stmt.run( + id, + hostId, + name, + image, + status, + state, + cpu_usage, + memory_usage + ); + return data; + }, + () => { + if ( + typeof id !== "string" || + typeof hostId !== "string" || + typeof name !== "string" || + typeof image !== "string" || + typeof status !== "string" || + typeof state !== "string" || + typeof cpu_usage !== "number" || + typeof memory_usage !== "number" + ) { + logger.error("Invalid parameter types for addContainerStats"); + throw new TypeError("Invalid parameter types for addContainerStats"); + } + } ); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Adding container statistics ✔️ (${duration}ms)`); - return data }, updateHostStats(stats: HostStats) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Update Host Stats ⏳") - const labelsJson = JSON.stringify(stats.labels); - const stmt = db.prepare(` - INSERT INTO host_stats ( - hostId, - dockerVersion, - apiVersion, - os, - architecture, - totalMemory, - totalCPU, - labels, - containers, - containersRunning, - containersStopped, - containersPaused, - images - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(hostId) DO UPDATE SET - dockerVersion = excluded.dockerVersion, - apiVersion = excluded.apiVersion, - os = excluded.os, - architecture = excluded.architecture, - totalMemory = excluded.totalMemory, - totalCPU = excluded.totalCPU, - labels = excluded.labels, - containers = excluded.containers, - containersRunning = excluded.containersRunning, - containersStopped = excluded.containersStopped, - containersPaused = excluded.containersPaused, - images = excluded.images; - `); - const data = stmt.run( - stats.hostId, - stats.dockerVersion, - stats.apiVersion, - stats.os, - stats.architecture, - stats.totalMemory, - stats.totalCPU, - labelsJson, - stats.containers, - stats.containersRunning, - stats.containersStopped, - stats.containersPaused, - stats.images, + return executeDbOperation( + "Update Host Stats", + () => { + const labelsJson = JSON.stringify(stats.labels); + const stmt = db.prepare(` + INSERT INTO host_stats ( + hostId, + dockerVersion, + apiVersion, + os, + architecture, + totalMemory, + totalCPU, + labels, + containers, + containersRunning, + containersStopped, + containersPaused, + images + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(hostId) DO UPDATE SET + dockerVersion = excluded.dockerVersion, + apiVersion = excluded.apiVersion, + os = excluded.os, + architecture = excluded.architecture, + totalMemory = excluded.totalMemory, + totalCPU = excluded.totalCPU, + labels = excluded.labels, + containers = excluded.containers, + containersRunning = excluded.containersRunning, + containersStopped = excluded.containersStopped, + containersPaused = excluded.containersPaused, + images = excluded.images; + `); + const data = stmt.run( + stats.hostId, + stats.dockerVersion, + stats.apiVersion, + stats.os, + stats.architecture, + stats.totalMemory, + stats.totalCPU, + labelsJson, + stats.containers, + stats.containersRunning, + stats.containersStopped, + stats.containersPaused, + stats.images + ); + return data; + }, + () => { } ); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Update Host stats ✔️ (${duration}ms)`); - return data }, - // Stacks: - // This is the stack config which will be saved in the database, the "real" docker-compose can be found in the designated folder addStack(stack_config: stacks_config) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Add Stack config ⏳") - - const stmt = db.prepare(` - INSERT INTO stacks_config ( - name, - version, - custom, - source, - container_count, - stack_prefix, - automatic_reboot_on_error, - image_updates - ) - VALUES(?, ?, ?, ?, ?, ?, ?, ?); - `); - - const data = stmt.run( - stack_config.name, - stack_config.version, - stack_config.custom, - stack_config.source, - stack_config.container_count, - stack_config.stack_prefix, - stack_config.automatic_reboot_on_error, - stack_config.image_updates + return executeDbOperation( + "Add Stack Config", + () => { + const stmt = db.prepare(` + INSERT INTO stacks_config ( + name, + version, + custom, + source, + container_count, + stack_prefix, + automatic_reboot_on_error, + image_updates + ) + VALUES(?, ?, ?, ?, ?, ?, ?, ?) + `); + const data = stmt.run( + stack_config.name, + stack_config.version, + stack_config.custom, + stack_config.source, + stack_config.container_count, + stack_config.stack_prefix, + stack_config.automatic_reboot_on_error, + stack_config.image_updates + ); + relayController.stackAdded(); + return data; + }, + () => { } ); - - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Add Stack config ✔️ (${duration}ms)`); - relayController.stackAdded(); - return data; }, getStacks() { - const startTime = Date.now(); - logger.debug("__task__ __db__ Get Stack config ⏳") - - const stmt = db.prepare(` + return executeDbOperation( + "Get Stacks", + () => { + const stmt = db.prepare(` SELECT name, version, custom, source, container_count, stack_prefix, automatic_reboot_on_error, image_updates FROM stacks_config ORDER BY name DESC `); - const data = stmt.all(); - - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Get Stack config ✔️ (${duration}ms)`); - return data; + const data = stmt.all(); + return data; + }, + () => { } + ); }, deleteStack(name: string) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Delete Stack config ⏳"); - - const stmt = db.prepare(` - DELETE FROM stacks_config - WHERE name = ?; - `); - const data = stmt.run(name); - - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Delete Stack config ✔️ (${duration}ms)`); - relayController.stackDeleted(); - return data; + return executeDbOperation( + "Delete Stack", + () => { + const stmt = db.prepare(` + DELETE FROM stacks_config + WHERE name = ?; + `); + const data = stmt.run(name); + relayController.stackDeleted(); + return data; + }, + () => { } + ); }, updateStack(stack_config: stacks_config) { - const startTime = Date.now(); - logger.debug("__task__ __db__ Update Stack config ⏳"); - - const stmt = db.prepare(` - UPDATE stacks_config - SET - version = ?, - custom = ?, - source = ?, - container_count = ?, - stack_prefix = ?, - automatic_reboot_on_error = ?, - image_updates = ? - WHERE name = ?; - `); - const data = stmt.run( - stack_config.version, - stack_config.custom, - stack_config.source, - stack_config.container_count, - stack_config.stack_prefix, - stack_config.automatic_reboot_on_error, - stack_config.image_updates, - stack_config.name + return executeDbOperation( + "Update Stack", + () => { + const stmt = db.prepare(` + UPDATE stacks_config + SET + version = ?, + custom = ?, + source = ?, + container_count = ?, + stack_prefix = ?, + automatic_reboot_on_error = ?, + image_updates = ? + WHERE name = ?; + `); + const data = stmt.run( + stack_config.version, + stack_config.custom, + stack_config.source, + stack_config.container_count, + stack_config.stack_prefix, + stack_config.automatic_reboot_on_error, + stack_config.image_updates, + stack_config.name + ); + relayController.stackUpdated(); + return data; + }, + () => { } ); - - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Update Stack config ✔️ (${duration}ms)`); - relayController.stackUpdated(); - return data; } -}; \ No newline at end of file +}; diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 82dc7894..b35aeeae 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -54,6 +54,7 @@ export const logger = createLogger({ format.timestamp({ format: "DD/MM HH:mm:ss" }), fileLineFormat(), format.printf(({ timestamp, level, message, file, line }) => { + const levelColors: Record = { error: chalk.red.bold, warn: chalk.yellow.bold, @@ -77,6 +78,19 @@ export const logger = createLogger({ const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); const coloredContext = chalk.cyan(`${file as string}:${line as number}`); const coloredTimestamp = chalk.yellow(timestamp); + + if (process.env.NODE_ENV !== "dev") { + return `${coloredLevel} [ ${coloredTimestamp} ] - ${chalk.gray( + message + )} - [ ${coloredContext} ]`; + } + + const prefix = `${paddedLevel} [ ${timestamp} ] - `; + const prefixLength = prefix.length; + const formattedMessage = formatTerminalMessage( + message as string, + prefixLength + ); const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; try { @@ -89,22 +103,9 @@ export const logger = createLogger({ } catch (error) { // Use console.error to avoid recursive logging console.error(`Error inserting log into DB: ${String(error)}`); - console.error("Aborting due to risk of recursion!") process.abort() } - if (process.env.NODE_ENV !== "dev") { - return `${coloredLevel} [ ${coloredTimestamp} ] - ${chalk.gray( - message - )} - [ ${coloredContext} ]`; - } - - const prefix = `${paddedLevel} [ ${timestamp} ] - `; - const prefixLength = prefix.length; - const formattedMessage = formatTerminalMessage( - message as string, - prefixLength - ); return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; }) ), From 91393d7c88a83f2ffefdbab2d0497d4d90e9cb76 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 12 Mar 2025 20:06:03 +0100 Subject: [PATCH 163/369] Fix: Robust parsing of Docker host URL. --- src/core/docker/client.ts | 52 ++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index 3d5baff4..ee0d7147 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -2,36 +2,60 @@ import type { DockerHost } from "~/typings/docker"; import Docker from "dockerode"; import { logger } from "~/core/utils/logger"; +async function fileExists(path: string): Promise { + try { + return await Bun.file(path).exists(); + } catch (error) { + return false; + } +} + export const getDockerClient = (host: DockerHost): Docker => { try { - const [hostAddress, port] = host.url.split(":"); - const protocol = host.secure ? "https" : "http"; + const inputUrl = host.url.includes("://") ? host.url : `${host.secure ? "https" : "http"}://${host.url}`; + const parsedUrl = new URL(inputUrl); + const hostAddress = parsedUrl.hostname; + let port = parsedUrl.port ? parseInt(parsedUrl.port) : (host.secure ? 2376 : 2375); + + if (isNaN(port) || port < 1 || port > 65535) { + throw new Error("Invalid port number in Docker host URL"); + } + return new Docker({ - protocol, + protocol: host.secure ? "https" : "http", host: hostAddress, - port: port ? parseInt(port) : host.secure ? 2376 : 2375, + port, version: "v1.41", // TODO: Add TLS configuration if needed }); } catch (error) { - logger.error("Invalid Docker host URL configuration,", error); + logger.error("Invalid Docker host URL configuration:", error); throw new Error("Invalid Docker host configuration"); } }; -export const stackClient = (): Docker => { +export const stackClient = async (): Promise => { + const socketPath = "/var/run/docker.sock"; try { - const docker = new Docker({ - socketPath: "/var/run/docker.sock" - }) + if (!(await fileExists(socketPath))) { + throw new Error("Docker socket not found at " + socketPath); + } - docker.ping().catch(() => { - throw new Error("Could not ping local Docker-Socket") + const docker = new Docker({ + socketPath }); + const pingTimeout = 2000; + await Promise.race([ + docker.ping(), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Ping timed out")), pingTimeout) + ) + ]); + return docker; } catch (error) { - logger.error(`Could not create Docker client for "/var/run/docker.sock" - ${error as string}`) - throw new Error() + logger.error(`Could not create Docker client for "${socketPath}" - ${error}`); + throw new Error("Failed to create Docker client for local Docker socket"); } -} \ No newline at end of file +}; From 583f5a90d2c63faeab725ddd50b212eab4cf6490 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 12 Mar 2025 20:13:49 +0100 Subject: [PATCH 164/369] Fix: Fixes #36 --- src/routes/docker-websocket.ts | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index e0406459..43c8038a 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -12,9 +12,14 @@ import { responseHandler } from "~/core/utils/respone-handler"; import type { DockerHost } from "~/typings/docker"; import split2 from "split2"; import type { Readable } from "stream"; -import type internal from "stream"; import type { streams } from "~/typings/websocket"; +interface ExtendedWebSocket extends WebSocket { + isOpen: boolean; + streams: any[]; + heartbeat: NodeJS.Timeout | null; +} + const set: { headers: HTTPHeaders; status?: number | keyof StatusMap } = { headers: {}, }; @@ -26,10 +31,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( socket.send(JSON.stringify({ message: "Connection established" })); let hosts: DockerHost[]; - // Track if the WebSocket is open - (socket as any).isOpen = true; - (socket as any).streams = []; - (socket as any).heartbeat = null; // Add heartbeat reference + (socket as unknown as ExtendedWebSocket).isOpen = true; + (socket as unknown as ExtendedWebSocket).streams = []; + (socket as unknown as ExtendedWebSocket).heartbeat = null; // Add heartbeat reference logger.info(`Opened WebSocket (${socket.id})`); @@ -54,7 +58,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( // Add heartbeat using WebSocket protocol-level ping (socket as any).heartbeat = setInterval(() => { - if (!(socket as any).isOpen) { + if (!(socket as unknown as ExtendedWebSocket).isOpen) { clearInterval((socket as any).heartbeat); return; } @@ -62,7 +66,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( }, 30000); for (const host of hosts) { - if (!(socket as any).isOpen) { + if (!(socket as unknown as ExtendedWebSocket).isOpen) { break }; @@ -79,7 +83,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( ); for (const containerInfo of containers) { - if (!(socket as any).isOpen) { + if (!(socket as unknown as ExtendedWebSocket).isOpen) { break }; @@ -97,7 +101,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( const splitStream = split2(); // Store both streams for cleanup - (socket as any).streams.push({ statsStream, splitStream }); + (socket as unknown as ExtendedWebSocket).streams.push({ statsStream, splitStream }); // Handle stream lifecycle statsStream @@ -195,7 +199,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( } }, - message(socket, message) { + message(_, message) { if (message === "pong") { return }; @@ -203,14 +207,14 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( close(socket, code, reason) { logger.info(`Closing SplitStream and WebSocket (${socket.id})`); - const wasOpen = (socket as any).isOpen; - (socket as any).isOpen = false; + const wasOpen = (socket as unknown as ExtendedWebSocket).isOpen; + (socket as unknown as ExtendedWebSocket).isOpen = false; // Immediate heartbeat cleanup clearInterval((socket as any).heartbeat); // Force-close streams using destructor pattern - const streams: streams[] = (socket as any).streams || []; + const streams: streams[] = (socket as unknown as ExtendedWebSocket).streams || []; streams.forEach(({ statsStream, splitStream }) => { try { // Immediate pipeline breakdown From bacd092a836adbc8af20f08dfdbfc600ca68fb0f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 14:49:40 +0100 Subject: [PATCH 165/369] Feat: Server timing - won't do OpenTelementry - Closes: #41 --- bun.lock | 3 + package.json | 1 + src/core/database/helper.ts | 26 +- src/core/database/repository.ts | 51 ++- src/core/docker/client.ts | 20 +- src/core/docker/relay-controller.ts | 14 +- src/index.ts | 19 +- src/routes/api-config.ts | 47 ++- src/routes/docker-websocket.ts | 28 +- src/routes/stacks.ts | 440 +++++++++---------- src/typings/docker-compose.ts | 628 +++++++++++++++------------- 11 files changed, 667 insertions(+), 610 deletions(-) diff --git a/bun.lock b/bun.lock index f91ae698..ba25a0f9 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "dockstatapi", "dependencies": { + "@elysiajs/server-timing": "^1.2.1", "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "chalk": "^5.4.1", @@ -34,6 +35,8 @@ "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], + "@elysiajs/server-timing": ["@elysiajs/server-timing@1.2.1", "", { "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-7i4xOSYRdgljxKg8fyyBPVtnwsjhvJBnJn4qpTiNXt6ElrW1V2FeV2rdhyw2AQagUknnfpbUXMeDLalPaDeaLQ=="], + "@elysiajs/static": ["@elysiajs/static@1.2.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-oLpAi8c+maPpA0XhhK3BELaIjIG+nXg/K9p8cFfW4q5ayRD59a3MOMOOGgpiXZkHJzLPWcouhhyyLAYtaANW4g=="], "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], diff --git a/package.json b/package.json index 06509479..d194b46d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f dockstatapi.db* && echo 'success'" }, "dependencies": { + "@elysiajs/server-timing": "^1.2.1", "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "chalk": "^5.4.1", diff --git a/src/core/database/helper.ts b/src/core/database/helper.ts index 3bea0446..ec9c1a75 100644 --- a/src/core/database/helper.ts +++ b/src/core/database/helper.ts @@ -1,17 +1,17 @@ import { logger } from "../utils/logger"; export function executeDbOperation( - label: string, - operation: () => T, - validate?: () => void + label: string, + operation: () => T, + validate?: () => void, ): T { - const startTime = Date.now(); - logger.debug(`__task__ __db__ ${label} �3`); - if (validate) { - validate(); - } - const result = operation(); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ ${label} �4f (${duration}ms)`); - return result; -} \ No newline at end of file + const startTime = Date.now(); + logger.debug(`__task__ __db__ ${label} �3`); + if (validate) { + validate(); + } + const result = operation(); + const duration = Date.now() - startTime; + logger.debug(`__task__ __db__ ${label} �4f (${duration}ms)`); + return result; +} diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 56543cf2..a2c01fba 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -74,7 +74,6 @@ export const dbFunctions = { logger.info("Starting server..."); - /* * Default values: * - Websocket polling interval 5 seconds @@ -106,7 +105,7 @@ export const dbFunctions = { ); stmt.run("Localhost", "localhost:2375", false); } - logger.debug("__task__ __db__ Initializing Database ⏳") + logger.debug("__task__ __db__ Initializing Database ⏳"); const duration = Date.now() - startTime; logger.debug(`__task__ __db__ Initializing Database ✔️ (${duration}ms)`); }, @@ -130,7 +129,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); } - } + }, ); }, @@ -146,7 +145,7 @@ export const dbFunctions = { const data = stmt.all(); return data as DockerHost[]; }, - () => { } + () => {}, ); }, @@ -185,7 +184,7 @@ export const dbFunctions = { const data = stmt.all(); return data; }, - () => { } + () => {}, ); }, @@ -207,7 +206,7 @@ export const dbFunctions = { logger.error("Level parameter must be a string"); throw new TypeError("Level parameter must be a string"); } - } + }, ); }, @@ -232,7 +231,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateDockerHost"); throw new TypeError("Invalid parameter types for updateDockerHost"); } - } + }, ); }, @@ -252,7 +251,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteDockerHost"); throw new TypeError("Name parameter must be a string"); } - } + }, ); }, @@ -266,7 +265,7 @@ export const dbFunctions = { const data = stmt.run(); return data; }, - () => { } + () => {}, ); }, @@ -286,14 +285,14 @@ export const dbFunctions = { logger.error("Invalid parameter type for clearLogsByLevel"); throw new TypeError("Level parameter must be a string"); } - } + }, ); }, updateConfig( polling_rate: number, fetching_interval: number, - keep_data_for: number + keep_data_for: number, ) { return executeDbOperation( "Update Config", @@ -316,7 +315,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateConfig"); throw new TypeError("Invalid parameter types for updateConfig"); } - } + }, ); }, @@ -331,7 +330,7 @@ export const dbFunctions = { const data = stmt.all(); return data; }, - () => { } + () => {}, ); }, @@ -356,7 +355,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteOldData"); throw new TypeError("Days parameter must be a number"); } - } + }, ); }, @@ -368,7 +367,7 @@ export const dbFunctions = { status: string, state: string, cpu_usage: number, - memory_usage: number + memory_usage: number, ) { return executeDbOperation( "Add Container Stats", @@ -385,7 +384,7 @@ export const dbFunctions = { status, state, cpu_usage, - memory_usage + memory_usage, ); return data; }, @@ -403,7 +402,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addContainerStats"); throw new TypeError("Invalid parameter types for addContainerStats"); } - } + }, ); }, @@ -456,11 +455,11 @@ export const dbFunctions = { stats.containersRunning, stats.containersStopped, stats.containersPaused, - stats.images + stats.images, ); return data; }, - () => { } + () => {}, ); }, @@ -489,12 +488,12 @@ export const dbFunctions = { stack_config.container_count, stack_config.stack_prefix, stack_config.automatic_reboot_on_error, - stack_config.image_updates + stack_config.image_updates, ); relayController.stackAdded(); return data; }, - () => { } + () => {}, ); }, @@ -510,7 +509,7 @@ export const dbFunctions = { const data = stmt.all(); return data; }, - () => { } + () => {}, ); }, @@ -526,7 +525,7 @@ export const dbFunctions = { relayController.stackDeleted(); return data; }, - () => { } + () => {}, ); }, @@ -554,12 +553,12 @@ export const dbFunctions = { stack_config.stack_prefix, stack_config.automatic_reboot_on_error, stack_config.image_updates, - stack_config.name + stack_config.name, ); relayController.stackUpdated(); return data; }, - () => { } + () => {}, ); - } + }, }; diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index ee0d7147..b97ef136 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -12,10 +12,16 @@ async function fileExists(path: string): Promise { export const getDockerClient = (host: DockerHost): Docker => { try { - const inputUrl = host.url.includes("://") ? host.url : `${host.secure ? "https" : "http"}://${host.url}`; + const inputUrl = host.url.includes("://") + ? host.url + : `${host.secure ? "https" : "http"}://${host.url}`; const parsedUrl = new URL(inputUrl); const hostAddress = parsedUrl.hostname; - let port = parsedUrl.port ? parseInt(parsedUrl.port) : (host.secure ? 2376 : 2375); + let port = parsedUrl.port + ? parseInt(parsedUrl.port) + : host.secure + ? 2376 + : 2375; if (isNaN(port) || port < 1 || port > 65535) { throw new Error("Invalid port number in Docker host URL"); @@ -42,20 +48,22 @@ export const stackClient = async (): Promise => { } const docker = new Docker({ - socketPath + socketPath, }); const pingTimeout = 2000; await Promise.race([ docker.ping(), new Promise((_, reject) => - setTimeout(() => reject(new Error("Ping timed out")), pingTimeout) - ) + setTimeout(() => reject(new Error("Ping timed out")), pingTimeout), + ), ]); return docker; } catch (error) { - logger.error(`Could not create Docker client for "${socketPath}" - ${error}`); + logger.error( + `Could not create Docker client for "${socketPath}" - ${error}`, + ); throw new Error("Failed to create Docker client for local Docker socket"); } }; diff --git a/src/core/docker/relay-controller.ts b/src/core/docker/relay-controller.ts index db8b6bb5..f99314d8 100644 --- a/src/core/docker/relay-controller.ts +++ b/src/core/docker/relay-controller.ts @@ -1,13 +1,7 @@ // Import any function here, when any of the specifies functions is detected, it will run said function export const relayController = { - stackAdded() { - - }, - stackDeleted() { - - }, - stackUpdated() { - - } -} \ No newline at end of file + stackAdded() {}, + stackDeleted() {}, + stackUpdated() {}, +}; diff --git a/src/index.ts b/src/index.ts index a2dfb814..975ebf9a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,9 +10,10 @@ import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; import { stackRoutes } from "./routes/stacks"; import { apiConfigRoutes } from "~/routes/api-config"; import { setSchedules } from "~/core/docker/scheduler"; +import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; -console.log("") +console.log(""); dbFunctions.init(); const DockStatAPI = new Elysia() @@ -47,6 +48,7 @@ const DockStatAPI = new Elysia() }, }), ) + .use(serverTiming()) .use(dockerRoutes) .use(dockerStatsRoutes) .use(backendLogs) @@ -55,11 +57,11 @@ const DockStatAPI = new Elysia() .use(stackRoutes) .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) .onError(({ code, set }) => { - if (code === 'NOT_FOUND') { - logger.warn("Unknown route, showing error page!") - set.status = 404 - set.headers['Content-Type'] = 'text/html' - return Bun.file('public/404.html') + if (code === "NOT_FOUND") { + logger.warn("Unknown route, showing error page!"); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); } }); @@ -67,7 +69,7 @@ async function startServer() { try { await loadPlugins("./src/plugins"); DockStatAPI.listen(3000, ({ hostname, port }) => { - console.log("----- [ ############## ]") + console.log("----- [ ############## ]"); logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( `Swagger API Documentation available at http://${hostname}:${port}/swagger`, @@ -83,5 +85,4 @@ await setSchedules(); await startServer(); logger.info("Started server"); -console.log("----- [ ############## ]") - +console.log("----- [ ############## ]"); diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index e6185517..1cdda5ec 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -67,29 +67,30 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], }, ) - .get("/package", async ({ set }) => { - try { - logger.debug("Fetching package.json"); - return { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error while reading package.json", - ); - } - }, + .get( + "/package", + async ({ set }) => { + try { + logger.debug("Fetching package.json"); + return { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error while reading package.json", + ); + } + }, { tags: ["Management"], }, diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 43c8038a..4b4fae7a 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -67,8 +67,8 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( for (const host of hosts) { if (!(socket as unknown as ExtendedWebSocket).isOpen) { - break - }; + break; + } logger.debug(`Processing host: ${host.name}`); @@ -84,8 +84,8 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( for (const containerInfo of containers) { if (!(socket as unknown as ExtendedWebSocket).isOpen) { - break - }; + break; + } logger.debug( `Processing container ${containerInfo.Id} on host ${host.name}`, @@ -101,7 +101,10 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( const splitStream = split2(); // Store both streams for cleanup - (socket as unknown as ExtendedWebSocket).streams.push({ statsStream, splitStream }); + (socket as unknown as ExtendedWebSocket).streams.push({ + statsStream, + splitStream, + }); // Handle stream lifecycle statsStream @@ -119,11 +122,11 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( .on("data", (line: string) => { // 1 = OPEN state if (socket.readyState !== 1) { - return - }; + return; + } if (!line) { - return - }; + return; + } try { const stats = JSON.parse(line); const cpuUsage = calculateCpuPercent(stats); @@ -201,8 +204,8 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( message(_, message) { if (message === "pong") { - return - }; + return; + } }, close(socket, code, reason) { @@ -214,7 +217,8 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( clearInterval((socket as any).heartbeat); // Force-close streams using destructor pattern - const streams: streams[] = (socket as unknown as ExtendedWebSocket).streams || []; + const streams: streams[] = + (socket as unknown as ExtendedWebSocket).streams || []; streams.forEach(({ statsStream, splitStream }) => { try { // Immediate pipeline breakdown diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 600dec55..3fcf9112 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -1,239 +1,239 @@ import { Elysia, error, t } from "elysia"; import { responseHandler } from "~/core/utils/respone-handler"; import { - deployStack, - stopStack, - pullStackImages, - restartStack, - getStackStatus, - startStack + deployStack, + stopStack, + pullStackImages, + restartStack, + getStackStatus, + startStack, } from "~/core/stacks/controller"; import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; export const stackRoutes = new Elysia({ prefix: "/stacks" }) - .post( - "/deploy", - async ({ set, body }) => { - try { - const isCustom = body.isCustom || false; + .post( + "/deploy", + async ({ set, body }) => { + try { + const isCustom = body.isCustom || false; + const image_updates = body.image_updates || false; - const image_updates = body.image_updates || false; - - - let missingParams: string[] = []; - if (!body.compose_spec) { - missingParams.push("compose_spec"); - } - if (!body.automatic_reboot_on_error) { - missingParams.push("automatic_reboot_on_error"); - } - if (!body.source) { - missingParams.push("source"); - } - if (!body.name) { - missingParams.push("name"); - } - - if (missingParams.length > 0) { - const errMsg = `Missing values of: ${missingParams.join("; ")}`; - return responseHandler.error(set, errMsg, errMsg); - } - - await deployStack( - body.compose_spec, - body.name, - body.version, - body.source, - body.automatic_reboot_on_error, - isCustom, - image_updates, - body.stack_prefix - ); - logger.info(`Deployed Stack (${body.name})`) - return responseHandler.ok( - set, - `Stack ${body.name} deployed successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error deploying stack" - ); - } - }, - { - detail: { tags: ["Stacks"] }, - body: t.Object({ - compose_spec: t.Any(), - name: t.String(), - version: t.Number(), - automatic_reboot_on_error: t.Boolean(), - isCustom: t.Boolean(), - image_updates: t.Boolean(), - source: t.String(), - stack_prefix: t.Optional(t.String()), - }), + let missingParams: string[] = []; + if (!body.compose_spec) { + missingParams.push("compose_spec"); } - ) - .post( - "/start", - async ({ set, body }) => { - try { - if (!body.stack) { - throw new Error("Stack needed") - } - await startStack(body.stack); - logger.info(`Started Stack (${body.stack})`) - return responseHandler.ok( - set, - `Stack ${body.stack} started successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error starting stack" - ); - } - }, - { - detail: { tags: ["Stacks"] }, - body: t.Object({ - stack: t.Any(), - }), + if (!body.automatic_reboot_on_error) { + missingParams.push("automatic_reboot_on_error"); } - ) - .post( - "/stop", - async ({ set, body }) => { - try { - if (!body.stack) { - throw new Error("Stack needed") - } - await stopStack(body.stack); - logger.info(`Stopped Stack (${body.stack})`) - return responseHandler.ok( - set, - `Stack ${body.stack} stopped successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error stopping stack" - ); - } - }, - { - detail: { tags: ["Stacks"] }, - body: t.Object({ - stack: t.Any(), - }), + if (!body.source) { + missingParams.push("source"); } - ) - .post( - "/restart", - async ({ set, body }) => { - try { - if (!body.stack) { - throw new Error("Stack needed") - } - await restartStack(body.stack); - logger.info(`Restarted Stack (${body.stack})`) - return responseHandler.ok( - set, - `Stack ${body.stack} restarted successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error restarting stack" - ); - } - }, - { - detail: { tags: ["Stacks"] }, - body: t.Object({ - stack: t.Any(), - }), + if (!body.name) { + missingParams.push("name"); } - ) - .post( - "/pull-images", - async ({ set, body }) => { - try { - if (!body.stack) { - throw new Error("Stack needed") - } - await pullStackImages(body.stack); - logger.info(`Pulled Stack images (${body.stack})`) - return responseHandler.ok( - set, - `Images for stack ${body.stack} pulled successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error pulling images" - ); - } - }, - { - detail: { tags: ["Stacks"] }, - body: t.Object({ - stack: t.Any(), - }), + + if (missingParams.length > 0) { + const errMsg = `Missing values of: ${missingParams.join("; ")}`; + return responseHandler.error(set, errMsg, errMsg); } - ) - .get( - "/status", - async ({ set, query }) => { - try { - if (!query.stack_name) { - throw new Error("Stack needed") - } - logger.debug(query.stack_name) - const status = await getStackStatus(query.stack_name); - const res = responseHandler.ok( - set, - `Stack ${query.stack_name} status retrieved successfully` - ); - logger.info("Fetched Stack status") - return { ...res, status: status }; - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error getting stack status" - ); - } - }, - { - detail: { tags: ["Stacks"] }, - query: t.Object({ - stack_name: t.Any(), - }), + + await deployStack( + body.compose_spec, + body.name, + body.version, + body.source, + body.automatic_reboot_on_error, + isCustom, + image_updates, + body.stack_prefix, + ); + logger.info(`Deployed Stack (${body.name})`); + return responseHandler.ok( + set, + `Stack ${body.name} deployed successfully`, + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error deploying stack", + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + compose_spec: t.Any(), + name: t.String(), + version: t.Number(), + automatic_reboot_on_error: t.Boolean(), + isCustom: t.Boolean(), + image_updates: t.Boolean(), + source: t.String(), + stack_prefix: t.Optional(t.String()), + }), + }, + ) + .post( + "/start", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed"); } - ) - .get("/", async ({ set }) => { - try { - const stacks = dbFunctions.getStacks(); - logger.info("Fetched Stacks") - return stacks; - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error getting stacks" - ); + await startStack(body.stack); + logger.info(`Started Stack (${body.stack})`); + return responseHandler.ok( + set, + `Stack ${body.stack} started successfully`, + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error starting stack", + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), + }, + ) + .post( + "/stop", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed"); } + await stopStack(body.stack); + logger.info(`Stopped Stack (${body.stack})`); + return responseHandler.ok( + set, + `Stack ${body.stack} stopped successfully`, + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error stopping stack", + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), }, - { - detail: { tags: ["Stacks"] }, + ) + .post( + "/restart", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed"); } - ); + await restartStack(body.stack); + logger.info(`Restarted Stack (${body.stack})`); + return responseHandler.ok( + set, + `Stack ${body.stack} restarted successfully`, + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error restarting stack", + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), + }, + ) + .post( + "/pull-images", + async ({ set, body }) => { + try { + if (!body.stack) { + throw new Error("Stack needed"); + } + await pullStackImages(body.stack); + logger.info(`Pulled Stack images (${body.stack})`); + return responseHandler.ok( + set, + `Images for stack ${body.stack} pulled successfully`, + ); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error pulling images", + ); + } + }, + { + detail: { tags: ["Stacks"] }, + body: t.Object({ + stack: t.Any(), + }), + }, + ) + .get( + "/status", + async ({ set, query }) => { + try { + if (!query.stack_name) { + throw new Error("Stack needed"); + } + logger.debug(query.stack_name); + const status = await getStackStatus(query.stack_name); + const res = responseHandler.ok( + set, + `Stack ${query.stack_name} status retrieved successfully`, + ); + logger.info("Fetched Stack status"); + return { ...res, status: status }; + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error getting stack status", + ); + } + }, + { + detail: { tags: ["Stacks"] }, + query: t.Object({ + stack_name: t.Any(), + }), + }, + ) + .get( + "/", + async ({ set }) => { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks"); + return stacks; + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error getting stacks", + ); + } + }, + { + detail: { tags: ["Stacks"] }, + }, + ); diff --git a/src/typings/docker-compose.ts b/src/typings/docker-compose.ts index a554c21c..9b23c2e4 100644 --- a/src/typings/docker-compose.ts +++ b/src/typings/docker-compose.ts @@ -1,30 +1,38 @@ export interface Stack { - compose_spec: ComposeSpec; - name: string - version: number; - source: string; + compose_spec: ComposeSpec; + name: string; + version: number; + source: string; } export interface ComposeSpec { - version?: string; - name?: string; - include?: Include[]; - services?: { [key: string]: Service }; - networks?: { [key: string]: Network }; - volumes?: { [key: string]: Volume }; - secrets?: { [key: string]: Secret }; - configs?: { [key: string]: Config }; - [key: `x-${string}`]: any; + version?: string; + name?: string; + include?: Include[]; + services?: { [key: string]: Service }; + networks?: { [key: string]: Network }; + volumes?: { [key: string]: Volume }; + secrets?: { [key: string]: Secret }; + configs?: { [key: string]: Config }; + [key: `x-${string}`]: any; } -type Include = string | { path: string | string[]; env_file?: string | string[]; project_directory?: string }; +type Include = + | string + | { + path: string | string[]; + env_file?: string | string[]; + project_directory?: string; + }; interface Service { - develop?: Development | null; - deploy?: Deployment | null; - annotations?: ListOrDict; - attach?: boolean | string; - build?: string | { + develop?: Development | null; + deploy?: Deployment | null; + annotations?: ListOrDict; + attach?: boolean | string; + build?: + | string + | { context?: string; dockerfile?: string; dockerfile_inline?: string; @@ -48,110 +56,125 @@ interface Service { ulimits?: Ulimits; platforms?: string[]; [key: `x-${string}`]: any; - }; - blkio_config?: { - device_read_bps?: BlkioLimit[]; - device_read_iops?: BlkioLimit[]; - device_write_bps?: BlkioLimit[]; - device_write_iops?: BlkioLimit[]; - weight?: number | string; - weight_device?: BlkioWeight[]; - }; - cap_add?: string[]; - cap_drop?: string[]; - cgroup?: 'host' | 'private'; - cgroup_parent?: string; - command?: Command; - configs?: ServiceConfigOrSecret[]; - container_name?: string; - cpu_count?: string | number; - cpu_percent?: string | number; - cpu_shares?: number | string; - cpu_quota?: number | string; - cpu_period?: number | string; - cpu_rt_period?: number | string; - cpu_rt_runtime?: number | string; - cpus?: number | string; - cpuset?: string; - credential_spec?: { - config?: string; - file?: string; - registry?: string; - [key: `x-${string}`]: any; - }; - depends_on?: string[] | { + }; + blkio_config?: { + device_read_bps?: BlkioLimit[]; + device_read_iops?: BlkioLimit[]; + device_write_bps?: BlkioLimit[]; + device_write_iops?: BlkioLimit[]; + weight?: number | string; + weight_device?: BlkioWeight[]; + }; + cap_add?: string[]; + cap_drop?: string[]; + cgroup?: "host" | "private"; + cgroup_parent?: string; + command?: Command; + configs?: ServiceConfigOrSecret[]; + container_name?: string; + cpu_count?: string | number; + cpu_percent?: string | number; + cpu_shares?: number | string; + cpu_quota?: number | string; + cpu_period?: number | string; + cpu_rt_period?: number | string; + cpu_rt_runtime?: number | string; + cpus?: number | string; + cpuset?: string; + credential_spec?: { + config?: string; + file?: string; + registry?: string; + [key: `x-${string}`]: any; + }; + depends_on?: + | string[] + | { [service: string]: { - condition: 'service_started' | 'service_healthy' | 'service_completed_successfully'; - restart?: boolean | string; - required?: boolean; - [key: `x-${string}`]: any; - } - }; - device_cgroup_rules?: string[]; - devices?: (string | { + condition: + | "service_started" + | "service_healthy" + | "service_completed_successfully"; + restart?: boolean | string; + required?: boolean; + [key: `x-${string}`]: any; + }; + }; + device_cgroup_rules?: string[]; + devices?: ( + | string + | { source: string; target?: string; permissions?: string; [key: `x-${string}`]: any; - })[]; - dns?: StringOrList; - dns_opt?: string[]; - dns_search?: StringOrList; - domainname?: string; - entrypoint?: Command; - env_file?: EnvFile; - label_file?: string | string[]; - environment?: ListOrDict; - expose?: (string | number)[]; - extends?: string | { service: string; file?: string }; - external_links?: string[]; - extra_hosts?: ExtraHosts; - gpus?: 'all' | Array<{ + } + )[]; + dns?: StringOrList; + dns_opt?: string[]; + dns_search?: StringOrList; + domainname?: string; + entrypoint?: Command; + env_file?: EnvFile; + label_file?: string | string[]; + environment?: ListOrDict; + expose?: (string | number)[]; + extends?: string | { service: string; file?: string }; + external_links?: string[]; + extra_hosts?: ExtraHosts; + gpus?: + | "all" + | Array<{ capabilities?: string[]; count?: string | number; device_ids?: string[]; driver?: string; options?: ListOrDict; [key: `x-${string}`]: any; - }>; - group_add?: (string | number)[]; - healthcheck?: Healthcheck; - hostname?: string; - image?: string; - init?: boolean | string; - ipc?: string; - isolation?: string; - labels?: ListOrDict; - links?: string[]; - logging?: { - driver?: string; - options?: { [key: string]: string | number | null }; - [key: `x-${string}`]: any; - }; - mac_address?: string; - mem_limit?: number | string; - mem_reservation?: string | number; - mem_swappiness?: number | string; - memswap_limit?: number | string; - network_mode?: string; - networks?: string[] | { + }>; + group_add?: (string | number)[]; + healthcheck?: Healthcheck; + hostname?: string; + image?: string; + init?: boolean | string; + ipc?: string; + isolation?: string; + labels?: ListOrDict; + links?: string[]; + logging?: { + driver?: string; + options?: { [key: string]: string | number | null }; + [key: `x-${string}`]: any; + }; + mac_address?: string; + mem_limit?: number | string; + mem_reservation?: string | number; + mem_swappiness?: number | string; + memswap_limit?: number | string; + network_mode?: string; + networks?: + | string[] + | { [network: string]: { - aliases?: string[]; - ipv4_address?: string; - ipv6_address?: string; - link_local_ips?: string[]; - mac_address?: string; - driver_opts?: { [key: string]: string | number }; - priority?: number; - [key: `x-${string}`]: any; + aliases?: string[]; + ipv4_address?: string; + ipv6_address?: string; + link_local_ips?: string[]; + mac_address?: string; + driver_opts?: { [key: string]: string | number }; + priority?: number; + [key: `x-${string}`]: any; } | null; - }; - oom_kill_disable?: boolean | string; - oom_score_adj?: string | number; - pid?: string | null; - pids_limit?: number | string; - platform?: string; - ports?: (number | string | { + }; + oom_kill_disable?: boolean | string; + oom_score_adj?: string | number; + pid?: string | null; + pids_limit?: number | string; + platform?: string; + ports?: ( + | number + | string + | { name?: string; mode?: string; host_ip?: string; @@ -160,234 +183,257 @@ interface Service { protocol?: string; app_protocol?: string; [key: `x-${string}`]: any; - })[]; - post_start?: ServiceHook[]; - pre_stop?: ServiceHook[]; - privileged?: boolean | string; - profiles?: string[]; - pull_policy?: 'always' | 'never' | 'if_not_present' | 'build' | 'missing'; - read_only?: boolean | string; - restart?: string; - runtime?: string; - scale?: number | string; - security_opt?: string[]; - shm_size?: number | string; - secrets?: ServiceConfigOrSecret[]; - sysctls?: ListOrDict; - stdin_open?: boolean | string; - stop_grace_period?: string; - stop_signal?: string; - storage_opt?: object; - tmpfs?: StringOrList; - tty?: boolean | string; - ulimits?: Ulimits; - user?: string; - uts?: string; - userns_mode?: string; - volumes?: (string | { + } + )[]; + post_start?: ServiceHook[]; + pre_stop?: ServiceHook[]; + privileged?: boolean | string; + profiles?: string[]; + pull_policy?: "always" | "never" | "if_not_present" | "build" | "missing"; + read_only?: boolean | string; + restart?: string; + runtime?: string; + scale?: number | string; + security_opt?: string[]; + shm_size?: number | string; + secrets?: ServiceConfigOrSecret[]; + sysctls?: ListOrDict; + stdin_open?: boolean | string; + stop_grace_period?: string; + stop_signal?: string; + storage_opt?: object; + tmpfs?: StringOrList; + tty?: boolean | string; + ulimits?: Ulimits; + user?: string; + uts?: string; + userns_mode?: string; + volumes?: ( + | string + | { type: string; source?: string; target?: string; read_only?: boolean | string; consistency?: string; bind?: { - propagation?: string; - create_host_path?: boolean | string; - recursive?: 'enabled' | 'disabled' | 'writable' | 'readonly'; - selinux?: 'z' | 'Z'; - [key: `x-${string}`]: any; + propagation?: string; + create_host_path?: boolean | string; + recursive?: "enabled" | "disabled" | "writable" | "readonly"; + selinux?: "z" | "Z"; + [key: `x-${string}`]: any; }; volume?: { - nocopy?: boolean | string; - subpath?: string; - [key: `x-${string}`]: any; + nocopy?: boolean | string; + subpath?: string; + [key: `x-${string}`]: any; }; tmpfs?: { - size?: number | string; - mode?: number | string; - [key: `x-${string}`]: any; + size?: number | string; + mode?: number | string; + [key: `x-${string}`]: any; }; [key: `x-${string}`]: any; - })[]; - volumes_from?: string[]; - working_dir?: string; - [key: `x-${string}`]: any; + } + )[]; + volumes_from?: string[]; + working_dir?: string; + [key: `x-${string}`]: any; } interface Healthcheck { - disable?: boolean | string; - interval?: string; - retries?: number | string; - test?: string | string[]; - timeout?: string; - start_period?: string; - start_interval?: string; - [key: `x-${string}`]: any; + disable?: boolean | string; + interval?: string; + retries?: number | string; + test?: string | string[]; + timeout?: string; + start_period?: string; + start_interval?: string; + [key: `x-${string}`]: any; } interface Development { - watch?: Array<{ - path: string; - action: 'rebuild' | 'sync' | 'restart' | 'sync+restart' | 'sync+exec'; - ignore?: string[]; - target?: string; - exec?: ServiceHook; - [key: `x-${string}`]: any; - }>; + watch?: Array<{ + path: string; + action: "rebuild" | "sync" | "restart" | "sync+restart" | "sync+exec"; + ignore?: string[]; + target?: string; + exec?: ServiceHook; [key: `x-${string}`]: any; + }>; + [key: `x-${string}`]: any; } interface Deployment { - mode?: string; - endpoint_mode?: string; - replicas?: number | string; - labels?: ListOrDict; - rollback_config?: { - parallelism?: number | string; - delay?: string; - failure_action?: string; - monitor?: string; - max_failure_ratio?: number | string; - order?: 'start-first' | 'stop-first'; - [key: `x-${string}`]: any; - }; - update_config?: { - parallelism?: number | string; - delay?: string; - failure_action?: string; - monitor?: string; - max_failure_ratio?: number | string; - order?: 'start-first' | 'stop-first'; - [key: `x-${string}`]: any; + mode?: string; + endpoint_mode?: string; + replicas?: number | string; + labels?: ListOrDict; + rollback_config?: { + parallelism?: number | string; + delay?: string; + failure_action?: string; + monitor?: string; + max_failure_ratio?: number | string; + order?: "start-first" | "stop-first"; + [key: `x-${string}`]: any; + }; + update_config?: { + parallelism?: number | string; + delay?: string; + failure_action?: string; + monitor?: string; + max_failure_ratio?: number | string; + order?: "start-first" | "stop-first"; + [key: `x-${string}`]: any; + }; + resources?: { + limits?: { + cpus?: number | string; + memory?: string; + pids?: number | string; + [key: `x-${string}`]: any; }; - resources?: { - limits?: { - cpus?: number | string; - memory?: string; - pids?: number | string; - [key: `x-${string}`]: any; - }; - reservations?: { - cpus?: number | string; - memory?: string; - generic_resources?: Array<{ - discrete_resource_spec?: { - kind?: string; - value?: number | string; - [key: `x-${string}`]: any; - }; - [key: `x-${string}`]: any; - }>; - devices?: Array<{ - capabilities?: string[]; - count?: string | number; - device_ids?: string[]; - driver?: string; - options?: ListOrDict; - [key: `x-${string}`]: any; - }>; - [key: `x-${string}`]: any; + reservations?: { + cpus?: number | string; + memory?: string; + generic_resources?: Array<{ + discrete_resource_spec?: { + kind?: string; + value?: number | string; + [key: `x-${string}`]: any; }; [key: `x-${string}`]: any; - }; - restart_policy?: { - condition?: string; - delay?: string; - max_attempts?: number | string; - window?: string; - [key: `x-${string}`]: any; - }; - placement?: { - constraints?: string[]; - preferences?: Array<{ - spread?: string; - [key: `x-${string}`]: any; - }>; - max_replicas_per_node?: number | string; + }>; + devices?: Array<{ + capabilities?: string[]; + count?: string | number; + device_ids?: string[]; + driver?: string; + options?: ListOrDict; [key: `x-${string}`]: any; + }>; + [key: `x-${string}`]: any; }; [key: `x-${string}`]: any; + }; + restart_policy?: { + condition?: string; + delay?: string; + max_attempts?: number | string; + window?: string; + [key: `x-${string}`]: any; + }; + placement?: { + constraints?: string[]; + preferences?: Array<{ + spread?: string; + [key: `x-${string}`]: any; + }>; + max_replicas_per_node?: number | string; + [key: `x-${string}`]: any; + }; + [key: `x-${string}`]: any; } type Command = string | string[] | null; -type EnvFile = string | Array; +type EnvFile = + | string + | Array< + string | { path: string; format?: string; required?: boolean | string } + >; type StringOrList = string | string[]; -type ListOrDict = { [key: string]: string | number | boolean | null } | string[]; +type ListOrDict = + | { [key: string]: string | number | boolean | null } + | string[]; type ExtraHosts = { [host: string]: string | string[] } | string[]; -interface BlkioLimit { path: string; rate: number | string; } -interface BlkioWeight { path: string; weight: number | string; } -type ServiceConfigOrSecret = string | { - source: string; - target?: string; - uid?: string; - gid?: string; - mode?: number | string; - [key: `x-${string}`]: any; +interface BlkioLimit { + path: string; + rate: number | string; +} +interface BlkioWeight { + path: string; + weight: number | string; +} +type ServiceConfigOrSecret = + | string + | { + source: string; + target?: string; + uid?: string; + gid?: string; + mode?: number | string; + [key: `x-${string}`]: any; + }; +type Ulimits = { + [key: string]: + | number + | string + | { hard: number | string; soft: number | string }; }; -type Ulimits = { [key: string]: number | string | { hard: number | string; soft: number | string } }; interface ServiceHook { - command?: Command; - user?: string; - privileged?: boolean | string; - working_dir?: string; - environment?: ListOrDict; - [key: `x-${string}`]: any; + command?: Command; + user?: string; + privileged?: boolean | string; + working_dir?: string; + environment?: ListOrDict; + [key: `x-${string}`]: any; } interface Network { - name?: string; + name?: string; + driver?: string; + driver_opts?: { [key: string]: string | number }; + ipam?: { driver?: string; - driver_opts?: { [key: string]: string | number }; - ipam?: { - driver?: string; - config?: Array<{ - subnet?: string; - ip_range?: string; - gateway?: string; - aux_addresses?: { [key: string]: string }; - [key: `x-${string}`]: any; - }>; - options?: { [key: string]: string }; - [key: `x-${string}`]: any; - }; - external?: boolean | string | { name?: string;[key: `x-${string}`]: any }; - internal?: boolean | string; - enable_ipv4?: boolean | string; - enable_ipv6?: boolean | string; - attachable?: boolean | string; - labels?: ListOrDict; + config?: Array<{ + subnet?: string; + ip_range?: string; + gateway?: string; + aux_addresses?: { [key: string]: string }; + [key: `x-${string}`]: any; + }>; + options?: { [key: string]: string }; [key: `x-${string}`]: any; + }; + external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; + internal?: boolean | string; + enable_ipv4?: boolean | string; + enable_ipv6?: boolean | string; + attachable?: boolean | string; + labels?: ListOrDict; + [key: `x-${string}`]: any; } interface Volume { - name?: string; - driver?: string; - driver_opts?: { [key: string]: string | number }; - external?: boolean | string | { name?: string;[key: `x-${string}`]: any }; - labels?: ListOrDict; - [key: `x-${string}`]: any; + name?: string; + driver?: string; + driver_opts?: { [key: string]: string | number }; + external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; + labels?: ListOrDict; + [key: `x-${string}`]: any; } interface Secret { - name?: string; - environment?: string; - file?: string; - external?: boolean | string | { name?: string;[key: string]: any }; - labels?: ListOrDict; - driver?: string; - driver_opts?: { [key: string]: string | number }; - template_driver?: string; - [key: `x-${string}`]: any; + name?: string; + environment?: string; + file?: string; + external?: boolean | string | { name?: string; [key: string]: any }; + labels?: ListOrDict; + driver?: string; + driver_opts?: { [key: string]: string | number }; + template_driver?: string; + [key: `x-${string}`]: any; } interface Config { - name?: string; - content?: string; - environment?: string; - file?: string; - external?: boolean | string | { name?: string;[key: string]: any }; - labels?: ListOrDict; - template_driver?: string; - [key: `x-${string}`]: any; -} \ No newline at end of file + name?: string; + content?: string; + environment?: string; + file?: string; + external?: boolean | string | { name?: string; [key: string]: any }; + labels?: ListOrDict; + template_driver?: string; + [key: `x-${string}`]: any; +} From d3219e582a99155df1f3d4a647b0938642a39ba0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 22:40:05 +0100 Subject: [PATCH 166/369] Feat: tRPC, ReadMe and Docs Update --- .github/DockStat.png | Bin 0 -> 79885 bytes .gitignore | 3 +- .knip.json | 4 + README.md | 85 ++------ bun.lock | 137 +++++++++++- package.json | 12 +- src/core/database/helper.ts | 6 +- src/core/database/repository.ts | 72 +++---- src/core/docker/client.ts | 40 +--- src/core/docker/relay-controller.ts | 7 - src/core/plugins/plugin-actions.ts | 3 - src/core/plugins/plugin-manager.ts | 8 +- src/core/trpc/README.md | 1 + src/core/trpc/index.ts | 4 + .../trpc/procedures/api-config.procedure.ts | 79 +++++++ .../procedures/docker-manager.procedure.ts | 65 ++++++ .../trpc/procedures/docker-stats.procedure.ts | 147 +++++++++++++ src/core/trpc/procedures/logs.procedure.ts | 73 +++++++ src/core/trpc/procedures/stacks.procedure.ts | 199 ++++++++++++++++++ src/core/trpc/router.ts | 21 ++ src/core/trpc/trpc.ts | 5 + src/index.ts | 9 +- src/plugins/example.plugin.ts | 1 - src/routes/api-config.ts | 21 +- src/routes/stacks.ts | 44 ++-- src/typings/database.ts | 2 +- 26 files changed, 841 insertions(+), 207 deletions(-) create mode 100644 .github/DockStat.png create mode 100644 .knip.json delete mode 100644 src/core/docker/relay-controller.ts create mode 100644 src/core/trpc/README.md create mode 100644 src/core/trpc/index.ts create mode 100644 src/core/trpc/procedures/api-config.procedure.ts create mode 100644 src/core/trpc/procedures/docker-manager.procedure.ts create mode 100644 src/core/trpc/procedures/docker-stats.procedure.ts create mode 100644 src/core/trpc/procedures/logs.procedure.ts create mode 100644 src/core/trpc/procedures/stacks.procedure.ts create mode 100644 src/core/trpc/router.ts create mode 100644 src/core/trpc/trpc.ts diff --git a/.github/DockStat.png b/.github/DockStat.png new file mode 100644 index 0000000000000000000000000000000000000000..d375bd49107c79a960488d6062276a72cf6bd512 GIT binary patch literal 79885 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⪼EggaS+_T%ej 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 KU+70|um2a&GMD85 literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore index aa426ba1..c61c6830 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.db* /stacks -/node_modules \ No newline at end of file +/node_modules +.test \ No newline at end of file diff --git a/.knip.json b/.knip.json new file mode 100644 index 00000000..14f7f539 --- /dev/null +++ b/.knip.json @@ -0,0 +1,4 @@ +{ + "entry": ["src/index.ts"], + "project": ["src/**/*.ts"] +} \ No newline at end of file diff --git a/README.md b/README.md index cbb25b7a..37881b75 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,31 @@ -# DockStat API v3 +![Logo](.github/DockStat.png) +![CC BY-NC 4.0 License](https://img.shields.io/badge/License-CC_BY--NC_4.0-lightgrey.svg) -! WIP Documentation ! +--- -## Usage +# DockStatAPI -The DockStat API provides the following endpoints: +Docker monitoring API with real-time statistics, stack management, and plugin support. -### Docker Containers -- `GET /docker/containers`: Retrieve statistics for all containers across all configured Docker hosts. +## Features -### Docker Hosts -- `GET /docker/hosts/:id`: Retrieve configuration and statistics for a specific Docker host. +- Real-time container metrics via WebSocket +- Multi-host Docker environment monitoring +- Compose stack deployment/management +- Plugin system for custom logic/notifications +- Historical stats storage (SQLite) +- Swagger API documentation +- Web dashboard (WIP) -### Docker Configuration -- `POST /docker-config/add-host`: Add a new Docker host. -- `POST /docker-config/update-host`: Update an existing Docker host. -- `GET /docker-config/hosts`: Retrieve a list of all configured Docker hosts. +## Tech Stack -### API Configuration -- `GET /config/get`: Retrieve the current API configuration. -- `POST /config/update`: Update the API configuration. +- **Runtime**: [Bun.sh](http://Bun.sh) +- **Framework**: [Elysia.js](https://elysiajs.com/) +- **Database**: SQLite (WAL mode) +- **Docker**: dockerode + compose +- **Monitoring**: Custom metrics collection +- **Auth**: (TODO - Currently open) -### Logs -- `GET /logs`: Retrieve all backend logs. -- `GET /logs/:level`: Retrieve logs filtered by log level. -- `DELETE /logs`: Clear all backend logs. -- `DELETE /logs/:level`: Clear logs by log level. +## Documentation and Wiki -### Websocket -- `WS(S) /docker/stats`: Retrieve the current API configuration. - -## API - -The DockStat API exposes the following endpoints: - -| Endpoint | Method | Description | -| --- | --- | --- | -| `/docker/containers` | `GET` | Retrieve statistics for all containers across all configured Docker hosts. | -| `/docker/hosts/:id` | `GET` | Retrieve configuration and statistics for a specific Docker host. | -| `/docker-config/add-host` | `POST` | Add a new Docker host. | -| `/docker-config/update-host` | `POST` | Update an existing Docker host. | -| `/docker-config/hosts` | `GET` | Retrieve a list of all configured Docker hosts. | -| `/config/get` | `GET` | Retrieve the current API configuration. | -| `/config/update` | `POST` | Update the API configuration. | -| `/logs` | `GET` | Retrieve all backend logs. | -| `/logs/:level` | `GET` | Retrieve logs filtered by log level. | -| `/logs` | `DELETE` | Clear all backend logs. | -| `/logs/:level` | `DELETE` | Clear logs by log level. | - -## Contributing - -1. Fork the repository. -2. Create a new branch for your feature or bug fix. -3. Make your changes and commit them. -4. Push your branch to your forked repository. -5. Submit a pull request to the main repository. - -## License - -This project is licensed under the CC BY-NC 4.0 License. -![cc-by-nc-image](https://licensebuttons.net/l/by-nc/4.0/88x31.png) - -## Testing - -To run the tests, execute the following command: -(Currently no tests configured!) -``` -bun test -``` - -This will run the test suite and report the results. +Please see [DockStatAPI](https://dockstatapi.itsnik.de) diff --git a/bun.lock b/bun.lock index ba25a0f9..5ab66752 100644 --- a/bun.lock +++ b/bun.lock @@ -7,20 +7,25 @@ "@elysiajs/server-timing": "^1.2.1", "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", + "@elysiajs/trpc": "^1.1.0", + "@elysiajs/websocket": "^0.2.8", + "@trpc/server": "^10.45.2", "chalk": "^5.4.1", "docker-compose": "^1.1.1", "dockerode": "^4.0.4", "elysia": "latest", "split2": "^4.2.0", "winston": "^3.17.0", - "winston-transport": "^4.9.0", "yaml": "^2.7.0", }, "devDependencies": { "@types/dockerode": "^3.3.34", + "@types/node": "^22.13.10", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", + "knip": "^5.46.0", + "typescript": "^5.8.2", "wrap-ansi": "^9.0.0", }, }, @@ -41,12 +46,22 @@ "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], + "@elysiajs/trpc": ["@elysiajs/trpc@1.1.0", "", { "peerDependencies": { "elysia": ">= 1.1.0" } }, "sha512-M8QWC+Wa5Z5MWY/+uMQuwZ+JoQkp4jOc1ra4SncFy1zSjFGin59LO1AT0pE+DRJaFV17gha9y7cB6Q7GnaJEAw=="], + + "@elysiajs/websocket": ["@elysiajs/websocket@0.2.8", "", { "dependencies": { "nanoid": "^4.0.0", "raikiri": "^0.0.0-beta.3" }, "peerDependencies": { "elysia": ">= 0.2.2" } }, "sha512-K9KLmYL1SYuAV353GvmK0V9DG5w7XTOGsa1H1dGB5BUTzvBaMvnwNeqnJQ3cjf9V1c0EjQds0Ty4LfUFvV45jw=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.12.6", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@4.0.1", "", { "dependencies": { "@nodelib/fs.stat": "4.0.0", "run-parallel": "^1.2.0" } }, "sha512-vAkI715yhnmiPupY+dq+xenu5Tdf2TBQ66jLvBIcCddtz+5Q8LbMKaf9CIJJreez8fQ8fgaY+RaywQx8RJIWpw=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@3.0.1", "", { "dependencies": { "@nodelib/fs.scandir": "4.0.1", "fastq": "^1.15.0" } }, "sha512-nIh/M6Kh3ZtOmlY00DaUYB4xeeV6F3/ts1l29iwl3/cfyY/OuCfUx+v08zgx8TKPTifXRcjjqVQ4KB2zOYSbyw=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], @@ -75,11 +90,15 @@ "@sinclair/typebox": ["@sinclair/typebox@0.34.27", "", {}, "sha512-C7mxE1VC3WC2McOufZXEU48IfRVI+BcKxk4NOyNn3+JMUNdJHEWGS5CqjuDX+ij2NCCz8/nse1mT7yn8Fv2GHg=="], + "@snyk/github-codeowners": ["@snyk/github-codeowners@1.1.0", "", { "dependencies": { "commander": "^4.1.1", "ignore": "^5.1.8", "p-map": "^4.0.0" }, "bin": { "github-codeowners": "dist/cli.js" } }, "sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw=="], + + "@trpc/server": ["@trpc/server@10.45.2", "", {}, "sha512-wOrSThNNE4HUnuhJG6PfDRp4L2009KDVxsd+2VYH8ro6o/7/jwYZ8Uu5j+VaW+mOmc8EHerHzGcdbGNQSAUPgg=="], + "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], "@types/dockerode": ["@types/dockerode@3.3.34", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-mH9SuIb8NuTDsMus5epcbTzSbEo52fKLBMo0zapzYIAIyfDqoIFn7L3trekHLKC8qmxGV++pPUP4YqQ9n5v2Zg=="], - "@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], @@ -91,10 +110,14 @@ "@unhead/schema": ["@unhead/schema@1.11.19", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg=="], - "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], @@ -105,6 +128,8 @@ "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], @@ -115,6 +140,8 @@ "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], @@ -129,6 +156,8 @@ "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], @@ -139,12 +168,16 @@ "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + "docker-compose": ["docker-compose@1.1.1", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-UkIUz0LtzuO17Ijm6SXMGtfZMs7IvbNwvuJBiBuN93PIhr/n9/sbJMqpvYFaCBGfwu1ZM4PPPDgQzeeke4lEoA=="], "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], "dockerode": ["dockerode@4.0.4", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.0.1", "uuid": "^10.0.0" } }, "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w=="], + "easy-table": ["easy-table@1.2.0", "", { "dependencies": { "ansi-regex": "^5.0.1" }, "optionalDependencies": { "wcwidth": "^1.0.1" } }, "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww=="], + "elysia": ["elysia@1.2.21", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-E9b1JcB7fiQ2ptk24W8OnBrMYUoKzffIXob9uTVUKhqOKxaXAd9UyWBeyr7JCDa/VD/b/9S8aIey9/YJsK5sLg=="], "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], @@ -153,10 +186,18 @@ "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], @@ -165,20 +206,40 @@ "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "knip": ["knip@5.46.0", "", { "dependencies": { "@nodelib/fs.walk": "3.0.1", "@snyk/github-codeowners": "1.1.0", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.0", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "summary": "2.1.0", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-WedHSK5xNBWYgm64Rt5B9b0CVXL2kRBcyCeet3NHgdv9en3QE4AWSDPEiX48NoPUBW3h//9S0VwLF5MG/MPi3g=="], + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], @@ -189,12 +250,20 @@ "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nan": ["nan@2.22.1", "", {}, "sha512-pfRR4ZcNTSm2ZFHaztuvbICf+hyiG6ecA06SfAxoPmuHjvMu0KUIae7Y8GyVkbBqeEIidsmXeYooWIX9+qjfRQ=="], + "nanoid": ["nanoid@4.0.2", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw=="], + "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], @@ -203,18 +272,36 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], + + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], + "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "raikiri": ["raikiri@0.0.0-beta.8", "", {}, "sha512-cH/yfvkiGkN8IBB2MkRHikpPurTnd2sMkQ/xtGpXrp3O76P4ppcWPb+86mJaBDzKaclLnSX+9NnT79D7ifH4/w=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], @@ -227,6 +314,8 @@ "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="], + "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], @@ -241,12 +330,20 @@ "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="], + + "summary": ["summary@2.1.0", "", {}, "sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw=="], + + "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], + "tar-fs": ["tar-fs@2.0.1", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.0.0" } }, "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], @@ -259,6 +356,8 @@ "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], @@ -279,16 +378,42 @@ "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], + "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], + + "zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="], + + "@nodelib/fs.scandir/@nodelib/fs.stat": ["@nodelib/fs.stat@4.0.0", "", {}, "sha512-ctr6bByzksKRCV0bavi8WoQevU6plSp2IkllIsEqaiKe2mwNNnaluhnRhcsgGZHrrHk57B3lf95MkLMO3STYcg=="], + "@scalar/themes/@scalar/types": ["@scalar/types@0.0.34", "", { "dependencies": { "@scalar/openapi-types": "0.1.8", "@unhead/schema": "^1.11.11" } }, "sha512-q01ctijmHArM5KOny2zU+sHfhpsgOAENrDENecK2TsQNn5FYLmFZouMKeW2M6F7KFLPZnFxUiL/rT88b6Rp/Kg=="], + "@types/docker-modem/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + + "@types/dockerode/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + + "@types/split2/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "@types/ssh2/@types/node": ["@types/node@18.19.76", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw=="], + "@types/ws/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + + "bun-types/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + + "fast-glob/@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "protobufjs/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + + "strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.8", "", {}, "sha512-iufA5/6hPCmRIVD2eh7qGpoKvoA08Gw/qUb2JECifBtAwA93fo7+1k9uHK440f2LMJsbxIzA+nv7RS0BmfiO/g=="], @@ -297,18 +422,16 @@ "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "fast-glob/@nodelib/fs.walk/@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], } } diff --git a/package.json b/package.json index d194b46d..55a9e43b 100644 --- a/package.json +++ b/package.json @@ -17,30 +17,36 @@ "build": "bun build --target bun src/index.ts --outdir ./dist", "clean": "bun run clean:win || bun run clean:lin", "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q dockstatapi.db* && echo 'success'", - "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f dockstatapi.db* && echo 'success'" + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f dockstatapi.db* && echo 'success'", + "knip": "knip" }, "dependencies": { "@elysiajs/server-timing": "^1.2.1", "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", + "@elysiajs/trpc": "^1.1.0", + "@elysiajs/websocket": "^0.2.8", + "@trpc/server": "^10.45.2", "chalk": "^5.4.1", "docker-compose": "^1.1.1", "dockerode": "^4.0.4", "elysia": "latest", "split2": "^4.2.0", "winston": "^3.17.0", - "winston-transport": "^4.9.0", "yaml": "^2.7.0" }, "devDependencies": { "@types/dockerode": "^3.3.34", + "@types/node": "^22.13.10", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", + "knip": "^5.46.0", + "typescript": "^5.8.2", "wrap-ansi": "^9.0.0" }, "module": "src/index.js", "trustedDependencies": [ "protobufjs" ] -} \ No newline at end of file +} diff --git a/src/core/database/helper.ts b/src/core/database/helper.ts index ec9c1a75..753bc892 100644 --- a/src/core/database/helper.ts +++ b/src/core/database/helper.ts @@ -3,15 +3,15 @@ import { logger } from "../utils/logger"; export function executeDbOperation( label: string, operation: () => T, - validate?: () => void, + validate?: () => void ): T { const startTime = Date.now(); - logger.debug(`__task__ __db__ ${label} �3`); + logger.debug(`__task__ __db__ ${label} ⏳`); if (validate) { validate(); } const result = operation(); const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ ${label} �4f (${duration}ms)`); + logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); return result; } diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index a2c01fba..37f6ba58 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -1,7 +1,6 @@ import { executeDbOperation } from "./helper"; import Database from "bun:sqlite"; import { logger } from "~/core/utils/logger"; -import { relayController } from "~/core/docker/relay-controller"; import type { DockerHost, HostStats } from "~/typings/docker"; import type { stacks_config } from "~/typings/database"; @@ -66,7 +65,6 @@ export const dbFunctions = { ); CREATE TABLE IF NOT EXISTS config ( - polling_rate NUMBER, keep_data_for NUMBER, fetching_interval NUMBER ); @@ -76,7 +74,6 @@ export const dbFunctions = { /* * Default values: - * - Websocket polling interval 5 seconds * - Data retention value for the database (logs and container stats) 7 days * - Data fetcher for the Database: 5 minutes */ @@ -87,8 +84,8 @@ export const dbFunctions = { logger.debug("Initializing default config"); const stmt = db.prepare( ` - INSERT INTO config (polling_rate, keep_data_for, fetching_interval) VALUES (5, 7, 5) - `, + INSERT INTO config (keep_data_for, fetching_interval) VALUES (7, 5) + ` ); stmt.run(); } @@ -101,7 +98,7 @@ export const dbFunctions = { const stmt = db.prepare( ` INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) - `, + ` ); stmt.run("Localhost", "localhost:2375", false); } @@ -129,7 +126,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); } - }, + } ); }, @@ -145,7 +142,7 @@ export const dbFunctions = { const data = stmt.all(); return data as DockerHost[]; }, - () => {}, + () => {} ); }, @@ -153,7 +150,7 @@ export const dbFunctions = { level: string, message: string, file_name: string, - line: number, + line: number ) => { if ( typeof level !== "string" || @@ -184,7 +181,7 @@ export const dbFunctions = { const data = stmt.all(); return data; }, - () => {}, + () => {} ); }, @@ -206,7 +203,7 @@ export const dbFunctions = { logger.error("Level parameter must be a string"); throw new TypeError("Level parameter must be a string"); } - }, + } ); }, @@ -231,7 +228,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateDockerHost"); throw new TypeError("Invalid parameter types for updateDockerHost"); } - }, + } ); }, @@ -251,7 +248,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteDockerHost"); throw new TypeError("Name parameter must be a string"); } - }, + } ); }, @@ -265,7 +262,7 @@ export const dbFunctions = { const data = stmt.run(); return data; }, - () => {}, + () => {} ); }, @@ -285,37 +282,31 @@ export const dbFunctions = { logger.error("Invalid parameter type for clearLogsByLevel"); throw new TypeError("Level parameter must be a string"); } - }, + } ); }, - updateConfig( - polling_rate: number, - fetching_interval: number, - keep_data_for: number, - ) { + updateConfig(fetching_interval: number, keep_data_for: number) { return executeDbOperation( "Update Config", () => { const stmt = db.prepare(` UPDATE config - SET polling_rate = ?, - fetching_interval = ?, + SET fetching_interval = ?, keep_data_for = ? `); - const data = stmt.run(polling_rate, fetching_interval, keep_data_for); + const data = stmt.run(fetching_interval, keep_data_for); return data; }, () => { if ( - typeof polling_rate !== "number" || typeof fetching_interval !== "number" || typeof keep_data_for !== "number" ) { logger.error("Invalid parameter types for updateConfig"); throw new TypeError("Invalid parameter types for updateConfig"); } - }, + } ); }, @@ -324,13 +315,13 @@ export const dbFunctions = { "Get Config", () => { const stmt = db.prepare(` - SELECT polling_rate, keep_data_for, fetching_interval + SELECT keep_data_for, fetching_interval FROM config `); const data = stmt.all(); return data; }, - () => {}, + () => {} ); }, @@ -355,7 +346,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteOldData"); throw new TypeError("Days parameter must be a number"); } - }, + } ); }, @@ -367,7 +358,7 @@ export const dbFunctions = { status: string, state: string, cpu_usage: number, - memory_usage: number, + memory_usage: number ) { return executeDbOperation( "Add Container Stats", @@ -384,7 +375,7 @@ export const dbFunctions = { status, state, cpu_usage, - memory_usage, + memory_usage ); return data; }, @@ -402,7 +393,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addContainerStats"); throw new TypeError("Invalid parameter types for addContainerStats"); } - }, + } ); }, @@ -455,11 +446,11 @@ export const dbFunctions = { stats.containersRunning, stats.containersStopped, stats.containersPaused, - stats.images, + stats.images ); return data; }, - () => {}, + () => {} ); }, @@ -488,12 +479,11 @@ export const dbFunctions = { stack_config.container_count, stack_config.stack_prefix, stack_config.automatic_reboot_on_error, - stack_config.image_updates, + stack_config.image_updates ); - relayController.stackAdded(); return data; }, - () => {}, + () => {} ); }, @@ -509,7 +499,7 @@ export const dbFunctions = { const data = stmt.all(); return data; }, - () => {}, + () => {} ); }, @@ -522,10 +512,9 @@ export const dbFunctions = { WHERE name = ?; `); const data = stmt.run(name); - relayController.stackDeleted(); return data; }, - () => {}, + () => {} ); }, @@ -553,12 +542,11 @@ export const dbFunctions = { stack_config.stack_prefix, stack_config.automatic_reboot_on_error, stack_config.image_updates, - stack_config.name, + stack_config.name ); - relayController.stackUpdated(); return data; }, - () => {}, + () => {} ); }, }; diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index b97ef136..010a2bd6 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -2,14 +2,6 @@ import type { DockerHost } from "~/typings/docker"; import Docker from "dockerode"; import { logger } from "~/core/utils/logger"; -async function fileExists(path: string): Promise { - try { - return await Bun.file(path).exists(); - } catch (error) { - return false; - } -} - export const getDockerClient = (host: DockerHost): Docker => { try { const inputUrl = host.url.includes("://") @@ -20,8 +12,8 @@ export const getDockerClient = (host: DockerHost): Docker => { let port = parsedUrl.port ? parseInt(parsedUrl.port) : host.secure - ? 2376 - : 2375; + ? 2376 + : 2375; if (isNaN(port) || port < 1 || port > 65535) { throw new Error("Invalid port number in Docker host URL"); @@ -39,31 +31,3 @@ export const getDockerClient = (host: DockerHost): Docker => { throw new Error("Invalid Docker host configuration"); } }; - -export const stackClient = async (): Promise => { - const socketPath = "/var/run/docker.sock"; - try { - if (!(await fileExists(socketPath))) { - throw new Error("Docker socket not found at " + socketPath); - } - - const docker = new Docker({ - socketPath, - }); - - const pingTimeout = 2000; - await Promise.race([ - docker.ping(), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Ping timed out")), pingTimeout), - ), - ]); - - return docker; - } catch (error) { - logger.error( - `Could not create Docker client for "${socketPath}" - ${error}`, - ); - throw new Error("Failed to create Docker client for local Docker socket"); - } -}; diff --git a/src/core/docker/relay-controller.ts b/src/core/docker/relay-controller.ts deleted file mode 100644 index f99314d8..00000000 --- a/src/core/docker/relay-controller.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Import any function here, when any of the specifies functions is detected, it will run said function - -export const relayController = { - stackAdded() {}, - stackDeleted() {}, - stackUpdated() {}, -}; diff --git a/src/core/plugins/plugin-actions.ts b/src/core/plugins/plugin-actions.ts index 0b2f9357..f914681d 100644 --- a/src/core/plugins/plugin-actions.ts +++ b/src/core/plugins/plugin-actions.ts @@ -4,7 +4,4 @@ export const pluginAction = { containerStart(containerInfo: any) { pluginManager.handleContainerStart(containerInfo); }, - metricsReceived(metrics: any) { - pluginManager.handleMetrics(metrics); - }, }; diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index 614604af..a81aa0ea 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -12,7 +12,7 @@ export class PluginManager extends EventEmitter { logger.debug(`Registered plugin: ${plugin.name}`); } catch (error) { logger.error( - `Registering plugin ${plugin.name} failed: ${error as string}`, + `Registering plugin ${plugin.name} failed: ${error as string}` ); } } @@ -28,6 +28,12 @@ export class PluginManager extends EventEmitter { }); } + handleContainerStart(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerStart?.(containerInfo); + }); + } + handleContainerExit(containerInfo: ContainerInfo) { this.plugins.forEach((plugin) => { plugin.onContainerExit?.(containerInfo); diff --git a/src/core/trpc/README.md b/src/core/trpc/README.md new file mode 100644 index 00000000..32bdb3f4 --- /dev/null +++ b/src/core/trpc/README.md @@ -0,0 +1 @@ +Please see: [DockStatAPI tRPC Routes Reference](https://outline.itsnik.de/s/dockstat/doc/trpc-2hzqJ7BvA0) diff --git a/src/core/trpc/index.ts b/src/core/trpc/index.ts new file mode 100644 index 00000000..7a13655b --- /dev/null +++ b/src/core/trpc/index.ts @@ -0,0 +1,4 @@ +import { trpc } from "@elysiajs/trpc"; +import { appRouter } from "./router"; + +export default trpc(appRouter, { endpoint: "/trpc" }); diff --git a/src/core/trpc/procedures/api-config.procedure.ts b/src/core/trpc/procedures/api-config.procedure.ts new file mode 100644 index 00000000..bf6cd401 --- /dev/null +++ b/src/core/trpc/procedures/api-config.procedure.ts @@ -0,0 +1,79 @@ +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; +import { + version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, +} from "~/core/utils/package-json"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { router, publicProcedure } from "../trpc"; +import { config } from "~/typings/database"; + +const configInputSchema = z.object({ + fetching_interval: z.number(), + keep_data_for: z.number(), +}); + +export const configProcedure = router({ + get: publicProcedure.query(() => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + logger.debug("tRPC: Fetched backend config"); + return distinct; + } catch (error) { + logger.error("tRPC config get error", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Error getting the DockStatAPI config", + cause: error, + }); + } + }), + + update: publicProcedure.input(configInputSchema).mutation(({ input }) => { + try { + const { fetching_interval, keep_data_for } = input; + dbFunctions.updateConfig(fetching_interval, keep_data_for); + return { success: true, message: "Updated DockStatAPI config" }; + } catch (error) { + logger.error("tRPC config update error", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Error updating the DockStatAPI config", + cause: error, + }); + } + }), + + package: publicProcedure.query(() => { + try { + logger.debug("tRPC: Fetching package.json"); + return { + version, + description, + license, + authorName, + authorEmail, + authorWebsite, + contributors, + dependencies, + devDependencies, + }; + } catch (error) { + logger.error("tRPC package info error", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Error while reading package.json", + cause: error, + }); + } + }), +}); diff --git a/src/core/trpc/procedures/docker-manager.procedure.ts b/src/core/trpc/procedures/docker-manager.procedure.ts new file mode 100644 index 00000000..958b31b8 --- /dev/null +++ b/src/core/trpc/procedures/docker-manager.procedure.ts @@ -0,0 +1,65 @@ +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { router, publicProcedure } from "../trpc"; + +const addHostInput = z.object({ + name: z.string(), + url: z.string(), + secure: z.boolean(), +}); + +const updateHostInput = z.object({ + name: z.string(), + url: z.string(), + secure: z.boolean(), +}); + +export const dockerManagerProcedure = router({ + addHost: publicProcedure.input(addHostInput).mutation(({ input }) => { + try { + const { name, url, secure } = input; + dbFunctions.addDockerHost(name, url, secure); + logger.debug(`Added docker host (${name})`); + return { success: true, message: `Added docker host (${name})` }; + } catch (error) { + logger.error("Error adding docker host", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Error adding docker host", + cause: error, + }); + } + }), + + updateHost: publicProcedure.input(updateHostInput).mutation(({ input }) => { + try { + const { name, url, secure } = input; + dbFunctions.updateDockerHost(name, url, secure); + return { success: true, message: `Updated docker host (${name})` }; + } catch (error) { + logger.error("Error updating docker host", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to update host", + cause: error, + }); + } + }), + + getHosts: publicProcedure.query(() => { + try { + const dockerHosts = dbFunctions.getDockerHosts(); + logger.debug("Retrieved docker hosts via tRPC"); + return dockerHosts; + } catch (error) { + logger.error("Error retrieving docker hosts", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to retrieve hosts", + cause: error, + }); + } + }), +}); diff --git a/src/core/trpc/procedures/docker-stats.procedure.ts b/src/core/trpc/procedures/docker-stats.procedure.ts new file mode 100644 index 00000000..017e880a --- /dev/null +++ b/src/core/trpc/procedures/docker-stats.procedure.ts @@ -0,0 +1,147 @@ +import Docker from "dockerode"; +import { dbFunctions } from "~/core/database/repository"; +import { getDockerClient } from "~/core/docker/client"; +import { + calculateCpuPercent, + calculateMemoryUsage, +} from "~/core/utils/calculations"; +import { logger } from "~/core/utils/logger"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { router, publicProcedure } from "../trpc"; +import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; +import type { DockerInfo } from "~/typings/dockerode"; + +export const dockerStatsProcedure = router({ + getContainers: publicProcedure.query(async () => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; + + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Docker host connection failed", + cause: pingError, + }); + } + + const hostContainers = await docker.listContainers({ all: true }); + + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + reject( + new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Error fetching container stats", + cause: error, + }) + ); + } + if (!stats) { + reject( + new TRPCError({ + code: "NOT_FOUND", + message: "No stats available", + }) + ); + } + resolve(stats as Docker.ContainerStats); + }); + } + ); + + containers.push({ + id: containerInfo.Id, + hostId: host.name, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + }); + } catch (containerError) { + logger.error( + "Error fetching container stats", + containerError + ); + } + }) + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (hostError) { + logger.error("Error fetching containers for host", hostError); + } + }) + ); + + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + logger.error("Error fetching containers", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to retrieve containers", + cause: error, + }); + } + }), + + getHostStats: publicProcedure + .input(z.object({ id: z.string() })) + .query(async ({ input }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const host = hosts.find((h) => h.name === input.id); + + if (!host) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Host (${input.id}) not found`, + }); + } + + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + logger.error("Error fetching host stats", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to retrieve host config", + cause: error, + }); + } + }), +}); diff --git a/src/core/trpc/procedures/logs.procedure.ts b/src/core/trpc/procedures/logs.procedure.ts new file mode 100644 index 00000000..520a2cb6 --- /dev/null +++ b/src/core/trpc/procedures/logs.procedure.ts @@ -0,0 +1,73 @@ +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { router, publicProcedure } from "../trpc"; + +const logLevelSchema = z.enum(["debug", "info", "warn", "error"]); + +export const logsProcedure = router({ + getAll: publicProcedure.query(() => { + try { + const logs = dbFunctions.getAllLogs(); + logger.debug("Retrieved all logs via tRPC"); + return logs; + } catch (error) { + logger.error("Failed to retrieve logs", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to retrieve logs", + cause: error, + }); + } + }), + + getByLevel: publicProcedure + .input(z.object({ level: logLevelSchema })) + .query(({ input }) => { + try { + const logs = dbFunctions.getLogsByLevel(input.level); + logger.debug(`Retrieved logs (level: ${input.level}) via tRPC`); + return logs; + } catch (error) { + logger.error("Failed to retrieve logs by level", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to retrieve logs by level", + cause: error, + }); + } + }), + + clearAll: publicProcedure.mutation(() => { + try { + dbFunctions.clearAllLogs(); + logger.debug("Cleared all logs via tRPC"); + return { success: true }; + } catch (error) { + logger.error("Failed to clear all logs", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Could not delete all logs", + cause: error, + }); + } + }), + + clearByLevel: publicProcedure + .input(z.object({ level: logLevelSchema })) + .mutation(({ input }) => { + try { + dbFunctions.clearLogsByLevel(input.level); + logger.debug(`Cleared logs (level: ${input.level}) via tRPC`); + return { success: true }; + } catch (error) { + logger.error("Failed to clear logs by level", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Could not clear logs by level", + cause: error, + }); + } + }), +}); diff --git a/src/core/trpc/procedures/stacks.procedure.ts b/src/core/trpc/procedures/stacks.procedure.ts new file mode 100644 index 00000000..6aad4e38 --- /dev/null +++ b/src/core/trpc/procedures/stacks.procedure.ts @@ -0,0 +1,199 @@ +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { router, publicProcedure } from "../trpc"; +import { + deployStack, + stopStack, + pullStackImages, + restartStack, + getStackStatus, + startStack, +} from "~/core/stacks/controller"; + +const deployStackInput = z.object({ + compose_spec: z.any(), + name: z.string(), + version: z.number(), + automatic_reboot_on_error: z.boolean(), + isCustom: z.boolean().optional(), + image_updates: z.boolean().optional(), + source: z.string(), + stack_prefix: z.string().optional(), +}); + +const stackOperationInput = z.object({ + stack: z.any(), +}); + +const stackStatusInput = z.object({ + stack_name: z.any(), +}); + +export const stacksProcedure = router({ + deploy: publicProcedure + .input(deployStackInput) + .mutation(async ({ input }) => { + try { + const missingParams = []; + if (!input.compose_spec) missingParams.push("compose_spec"); + if (!input.automatic_reboot_on_error) + missingParams.push("automatic_reboot_on_error"); + if (!input.source) missingParams.push("source"); + if (!input.name) missingParams.push("name"); + + if (missingParams.length > 0) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Missing values: ${missingParams.join(", ")}`, + }); + } + + await deployStack( + input.compose_spec, + input.name, + input.version, + input.source, + input.automatic_reboot_on_error, + input.isCustom || false, + input.image_updates || false, + input.stack_prefix + ); + + logger.info(`Deployed Stack (${input.name}) via tRPC`); + return { + success: true, + message: `Stack ${input.name} deployed successfully`, + }; + } catch (error) { + logger.error("Error deploying stack", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error ? error.message : "Error deploying stack", + cause: error, + }); + } + }), + + start: publicProcedure + .input(stackOperationInput) + .mutation(async ({ input }) => { + try { + await startStack(input.stack); + logger.info(`Started Stack (${input.stack}) via tRPC`); + return { + success: true, + message: `Stack ${input.stack} started successfully`, + }; + } catch (error) { + logger.error("Error starting stack", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error ? error.message : "Error starting stack", + cause: error, + }); + } + }), + + stop: publicProcedure + .input(stackOperationInput) + .mutation(async ({ input }) => { + try { + await stopStack(input.stack); + logger.info(`Stopped Stack (${input.stack}) via tRPC`); + return { + success: true, + message: `Stack ${input.stack} stopped successfully`, + }; + } catch (error) { + logger.error("Error stopping stack", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error ? error.message : "Error stopping stack", + cause: error, + }); + } + }), + + restart: publicProcedure + .input(stackOperationInput) + .mutation(async ({ input }) => { + try { + await restartStack(input.stack); + logger.info(`Restarted Stack (${input.stack}) via tRPC`); + return { + success: true, + message: `Stack ${input.stack} restarted successfully`, + }; + } catch (error) { + logger.error("Error restarting stack", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error ? error.message : "Error restarting stack", + cause: error, + }); + } + }), + + pullImages: publicProcedure + .input(stackOperationInput) + .mutation(async ({ input }) => { + try { + await pullStackImages(input.stack); + logger.info(`Pulled Stack images (${input.stack}) via tRPC`); + return { + success: true, + message: `Images for stack ${input.stack} pulled successfully`, + }; + } catch (error) { + logger.error("Error pulling images", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error ? error.message : "Error pulling images", + cause: error, + }); + } + }), + + getStatus: publicProcedure + .input(stackStatusInput) + .query(async ({ input }) => { + try { + const status = await getStackStatus(input.stack_name); + logger.info(`Fetched Stack status (${input.stack_name}) via tRPC`); + return { status }; + } catch (error) { + logger.error("Error getting stack status", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error + ? error.message + : "Error getting stack status", + cause: error, + }); + } + }), + + getAll: publicProcedure.query(() => { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks via tRPC"); + return stacks; + } catch (error) { + logger.error("Error getting stacks", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error ? error.message : "Error getting stacks", + cause: error, + }); + } + }), +}); diff --git a/src/core/trpc/router.ts b/src/core/trpc/router.ts new file mode 100644 index 00000000..acdd78d0 --- /dev/null +++ b/src/core/trpc/router.ts @@ -0,0 +1,21 @@ +import { router, t } from "./trpc"; +import { configProcedure } from "./procedures/api-config.procedure"; +import { dockerManagerProcedure } from "./procedures/docker-manager.procedure"; +import { dockerStatsProcedure } from "./procedures/docker-stats.procedure"; +import { logsProcedure } from "./procedures/logs.procedure"; +import { stacksProcedure } from "./procedures/stacks.procedure"; + +export const appRouter = router({ + config: configProcedure, + docker: router({ + manager: dockerManagerProcedure, + stats: dockerStatsProcedure, + }), + logs: logsProcedure, + stacks: stacksProcedure, + health: router({ + check: t.procedure.query(() => ({ status: "healthy" })), + }), +}); + +export type AppRouter = typeof appRouter; diff --git a/src/core/trpc/trpc.ts b/src/core/trpc/trpc.ts new file mode 100644 index 00000000..554f58dd --- /dev/null +++ b/src/core/trpc/trpc.ts @@ -0,0 +1,5 @@ +import { initTRPC } from "@trpc/server"; + +export const t = initTRPC.create(); +export const router = t.router; +export const publicProcedure = t.procedure; diff --git a/src/index.ts b/src/index.ts index 975ebf9a..482f924a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { apiConfigRoutes } from "~/routes/api-config"; import { setSchedules } from "~/core/docker/scheduler"; import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; +import trpcRouter from "~/core/trpc"; console.log(""); dbFunctions.init(); @@ -46,9 +47,10 @@ const DockStatAPI = new Elysia() }, ], }, - }), + }) ) .use(serverTiming()) + .use(trpcRouter) .use(dockerRoutes) .use(dockerStatsRoutes) .use(backendLogs) @@ -72,7 +74,10 @@ async function startServer() { console.log("----- [ ############## ]"); logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + `Swagger API Documentation available at http://${hostname}:${port}/swagger` + ); + logger.info( + `tRPC Endpoint available at: http://${hostname}:${port}/trpc` ); }); } catch (error) { diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index a9ed6acc..bd71becc 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -1,7 +1,6 @@ import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo } from "~/typings/docker"; import type { HostStats } from "~/typings/docker"; -import { logger } from "~/core/utils/logger"; const ExamplePlugin: Plugin = { name: "Example Plugin", diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 1cdda5ec..bc081320 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -30,42 +30,37 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, "Error getting the DockStatAPI config", - error as string, + error as string ); } }, { tags: ["Management"], - }, + } ) .post( "/update", async ({ set, body }) => { try { - const { polling_rate, fetching_interval, keep_data_for } = body; + const { fetching_interval, keep_data_for } = body; set.headers["Content-Type"] = "application/json"; - dbFunctions.updateConfig( - polling_rate, - fetching_interval, - keep_data_for, - ); + dbFunctions.updateConfig(fetching_interval, keep_data_for); return responseHandler.ok(set, "Updated DockStatAPI config"); } catch (error) { return responseHandler.error( set, "Error updating the DockStatAPI config", - error as string, + error as string ); } }, { body: t.Object({ - polling_rate: t.Number(), fetching_interval: t.Number(), keep_data_for: t.Number(), }), tags: ["Management"], - }, + } ) .get( "/package", @@ -87,11 +82,11 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error while reading package.json", + "Error while reading package.json" ); } }, { tags: ["Management"], - }, + } ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 3fcf9112..f4be7b57 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -1,4 +1,4 @@ -import { Elysia, error, t } from "elysia"; +import { Elysia, t } from "elysia"; import { responseHandler } from "~/core/utils/respone-handler"; import { deployStack, @@ -47,18 +47,18 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body.automatic_reboot_on_error, isCustom, image_updates, - body.stack_prefix, + body.stack_prefix ); logger.info(`Deployed Stack (${body.name})`); return responseHandler.ok( set, - `Stack ${body.name} deployed successfully`, + `Stack ${body.name} deployed successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error deploying stack", + "Error deploying stack" ); } }, @@ -74,7 +74,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) source: t.String(), stack_prefix: t.Optional(t.String()), }), - }, + } ) .post( "/start", @@ -87,13 +87,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Started Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} started successfully`, + `Stack ${body.stack} started successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error starting stack", + "Error starting stack" ); } }, @@ -102,7 +102,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .post( "/stop", @@ -115,13 +115,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Stopped Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} stopped successfully`, + `Stack ${body.stack} stopped successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error stopping stack", + "Error stopping stack" ); } }, @@ -130,7 +130,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .post( "/restart", @@ -143,13 +143,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Restarted Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} restarted successfully`, + `Stack ${body.stack} restarted successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error restarting stack", + "Error restarting stack" ); } }, @@ -158,7 +158,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .post( "/pull-images", @@ -171,13 +171,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Pulled Stack images (${body.stack})`); return responseHandler.ok( set, - `Images for stack ${body.stack} pulled successfully`, + `Images for stack ${body.stack} pulled successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error pulling images", + "Error pulling images" ); } }, @@ -186,7 +186,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .get( "/status", @@ -199,7 +199,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) const status = await getStackStatus(query.stack_name); const res = responseHandler.ok( set, - `Stack ${query.stack_name} status retrieved successfully`, + `Stack ${query.stack_name} status retrieved successfully` ); logger.info("Fetched Stack status"); return { ...res, status: status }; @@ -207,7 +207,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stack status", + "Error getting stack status" ); } }, @@ -216,7 +216,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) query: t.Object({ stack_name: t.Any(), }), - }, + } ) .get( "/", @@ -229,11 +229,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stacks", + "Error getting stacks" ); } }, { detail: { tags: ["Stacks"] }, - }, + } ); diff --git a/src/typings/database.ts b/src/typings/database.ts index 3d95b353..c5200e60 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -7,10 +7,10 @@ interface backend_log_entries { } interface config { - polling_rate: number; keep_data_for: number; fetching_interval: number; } + interface stacks_config { name: string; version: number; From 4f195b4983557da57efd0447688f86f3cf1bf0e0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:03:02 +0100 Subject: [PATCH 167/369] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 37881b75..87b7750f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Docker monitoring API with real-time statistics, stack management, and plugin su - Plugin system for custom logic/notifications - Historical stats storage (SQLite) - Swagger API documentation -- Web dashboard (WIP) +- Web dashboard ([DockStat](https://github.com/its4nik/DockStat)) ## Tech Stack From a37465096494653b353646c6f9fd0c775d738ad8 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:12:11 +0100 Subject: [PATCH 168/369] Feat: Dependency graph --- .github/scripts/generate-mermaid.js | 25 ++++++++++++++++++ .github/worrkflows/dependency-graph.yml | 35 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 .github/scripts/generate-mermaid.js create mode 100644 .github/worrkflows/dependency-graph.yml diff --git a/.github/scripts/generate-mermaid.js b/.github/scripts/generate-mermaid.js new file mode 100644 index 00000000..d030db13 --- /dev/null +++ b/.github/scripts/generate-mermaid.js @@ -0,0 +1,25 @@ +const fs = require('fs'); +const path = require('path'); + +const dependencies = JSON.parse(fs.readFileSync('dependencies.json', 'utf-8')); + +const edges = []; +Object.entries(dependencies).forEach(([file, deps]) => { + deps.forEach(dep => { + edges.push(` "${file}" --> "${dep}"`); + }); +}); + +const mermaidContent = `graph TD +${edges.join('\n')}`; + +const markdownDoc = `# DockStatAPI Dependency Graph + +\`\`\`mermaid +${mermaidContent} +\`\`\` +`; + +fs.writeFileSync(path.join("./", 'dependencies.md'), markdownDoc); + +console.log('Successfully generated dependency graph'); \ No newline at end of file diff --git a/.github/worrkflows/dependency-graph.yml b/.github/worrkflows/dependency-graph.yml new file mode 100644 index 00000000..630c24bb --- /dev/null +++ b/.github/worrkflows/dependency-graph.yml @@ -0,0 +1,35 @@ +name: Generate Dependency Graph + +on: + push: + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install madge + run: npm install -g madge + + - name: Generate Dependency Data + run: madge --json src/index.ts > dependencies.json + + - name: Generate Mermaid Markdown + run: node .github/scripts/generate-mermaid.js + + - name: Commit and Push Changes + uses: EndBug/add-and-commit@v9 + with: + add: 'dependencies.md' + message: 'CI/CD: Update dependency graph' + committer_name: 'GitHub Action' + committer_email: 'action@github.com' \ No newline at end of file From 31b5e3039bcc1268248ae9beaa709baf049c0176 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:14:07 +0100 Subject: [PATCH 169/369] Fix: Typo --- .github/{worrkflows => workflows}/dependency-graph.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{worrkflows => workflows}/dependency-graph.yml (100%) diff --git a/.github/worrkflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml similarity index 100% rename from .github/worrkflows/dependency-graph.yml rename to .github/workflows/dependency-graph.yml From 29af2cf23742fb94e2d250b833a06b5ac5be7a55 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:15:32 +0100 Subject: [PATCH 170/369] Fix: Permission --- .github/workflows/dependency-graph.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 630c24bb..86d8e36d 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -3,6 +3,8 @@ name: Generate Dependency Graph on: push: +permissions: write-all + jobs: generate: runs-on: ubuntu-latest @@ -29,7 +31,7 @@ jobs: - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: - add: 'dependencies.md' - message: 'CI/CD: Update dependency graph' - committer_name: 'GitHub Action' - committer_email: 'action@github.com' \ No newline at end of file + add: "dependencies.md" + message: "CI/CD: Update dependency graph" + committer_name: "GitHub Action" + committer_email: "action@github.com" From 7b8c2c147bd057bdd426f996e77abc1257837e33 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 13 Mar 2025 22:15:55 +0000 Subject: [PATCH 171/369] CI/CD: Update dependency graph --- dependencies.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 dependencies.md diff --git a/dependencies.md b/dependencies.md new file mode 100644 index 00000000..fbb7268f --- /dev/null +++ b/dependencies.md @@ -0,0 +1,6 @@ +# DockStatAPI Dependency Graph + +```mermaid +graph TD + "index.ts" --> "routes/stacks.ts" +``` From 9139b03b72878c38b597f76a61bcd1452f170a76 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:23:13 +0100 Subject: [PATCH 172/369] Fix: Switch to dependency-cruiser --- .github/scripts/generate-mermaid.js | 25 ------------------------- .github/workflows/dependency-graph.yml | 24 ++++++++++++++---------- 2 files changed, 14 insertions(+), 35 deletions(-) delete mode 100644 .github/scripts/generate-mermaid.js diff --git a/.github/scripts/generate-mermaid.js b/.github/scripts/generate-mermaid.js deleted file mode 100644 index d030db13..00000000 --- a/.github/scripts/generate-mermaid.js +++ /dev/null @@ -1,25 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const dependencies = JSON.parse(fs.readFileSync('dependencies.json', 'utf-8')); - -const edges = []; -Object.entries(dependencies).forEach(([file, deps]) => { - deps.forEach(dep => { - edges.push(` "${file}" --> "${dep}"`); - }); -}); - -const mermaidContent = `graph TD -${edges.join('\n')}`; - -const markdownDoc = `# DockStatAPI Dependency Graph - -\`\`\`mermaid -${mermaidContent} -\`\`\` -`; - -fs.writeFileSync(path.join("./", 'dependencies.md'), markdownDoc); - -console.log('Successfully generated dependency graph'); \ No newline at end of file diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 86d8e36d..2a87e56f 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -11,27 +11,31 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - - name: Install madge - run: npm install -g madge + - name: Install dependency-cruiser and graphviz + run: | + npm install -g dependency-cruiser + sudo apt-get install -y graphviz - - name: Generate Dependency Data - run: madge --json src/index.ts > dependencies.json + - name: Generate Mermaid Diagram + run: | + npx depcruise src --include-only "^src" --output-type mermaid > dependency-graph.mmd + echo "Mermaid diagram generated at dependency-graph.mmd" - - name: Generate Mermaid Markdown - run: node .github/scripts/generate-mermaid.js + - name: Generate SVG Dependency Graph + run: | + npx depcruise src --include-only "^src" --output-type dot | dot -T svg > dependency-graph.svg + echo "SVG graph generated at docs/dependency-graph.svg" - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: - add: "dependencies.md" - message: "CI/CD: Update dependency graph" + add: "docs/*" + message: "Update dependency graphs" committer_name: "GitHub Action" committer_email: "action@github.com" From 8066901a7c0e80b4e4375a2e2e40d3d08a17a537 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:23:37 +0100 Subject: [PATCH 173/369] Fix: Adjust pathing --- .github/workflows/dependency-graph.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 2a87e56f..be00d51e 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -35,7 +35,7 @@ jobs: - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: - add: "docs/*" + add: "dependency-graph*" message: "Update dependency graphs" committer_name: "GitHub Action" committer_email: "action@github.com" From 45579b08cbe526736435a90d550b6dd42c4fb067 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:24:25 +0100 Subject: [PATCH 174/369] Fix: Adjust to no config --- .github/workflows/dependency-graph.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index be00d51e..256bf26d 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -24,12 +24,12 @@ jobs: - name: Generate Mermaid Diagram run: | - npx depcruise src --include-only "^src" --output-type mermaid > dependency-graph.mmd + npx depcruise src --no-config --include-only "^src" --output-type mermaid > dependency-graph.mmd echo "Mermaid diagram generated at dependency-graph.mmd" - name: Generate SVG Dependency Graph run: | - npx depcruise src --include-only "^src" --output-type dot | dot -T svg > dependency-graph.svg + npx depcruise src --no-config --include-only "^src" --output-type dot | dot -T svg > dependency-graph.svg echo "SVG graph generated at docs/dependency-graph.svg" - name: Commit and Push Changes From afc46f24a8043ea3d37ce4ea2b411a47601bdde0 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 13 Mar 2025 22:24:52 +0000 Subject: [PATCH 175/369] Update dependency graphs --- dependency-graph.mmd | 4 ++++ dependency-graph.svg | 13 +++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 dependency-graph.mmd create mode 100644 dependency-graph.svg diff --git a/dependency-graph.mmd b/dependency-graph.mmd new file mode 100644 index 00000000..b4f50c29 --- /dev/null +++ b/dependency-graph.mmd @@ -0,0 +1,4 @@ +flowchart LR + + + diff --git a/dependency-graph.svg b/dependency-graph.svg new file mode 100644 index 00000000..b27cd795 --- /dev/null +++ b/dependency-graph.svg @@ -0,0 +1,13 @@ + + + + + + +dependency-cruiser output + + + From 5e4a12f6e5af042ed64cdfbe15c5d77baf820697 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:28:28 +0100 Subject: [PATCH 176/369] Fix: Chhhhange to bun run --- .github/workflows/dependency-graph.yml | 10 +-- bun.lock | 105 +++++++++++++++++++++++-- dependency-graph.mmd | Bin 0 -> 2660 bytes package.json | 1 + 4 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 dependency-graph.mmd diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 256bf26d..86310904 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -13,23 +13,21 @@ jobs: uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 + uses: oven-sh/setup-bun@v2 - name: Install dependency-cruiser and graphviz run: | - npm install -g dependency-cruiser + bun add dependency-cruiser sudo apt-get install -y graphviz - name: Generate Mermaid Diagram run: | - npx depcruise src --no-config --include-only "^src" --output-type mermaid > dependency-graph.mmd + bun run depcruise src --no-config --include-only "^src" --output-type mermaid > dependency-graph.mmd echo "Mermaid diagram generated at dependency-graph.mmd" - name: Generate SVG Dependency Graph run: | - npx depcruise src --no-config --include-only "^src" --output-type dot | dot -T svg > dependency-graph.svg + bun run depcruise src --no-config --include-only "^src" --output-type dot | dot -T svg > dependency-graph.svg echo "SVG graph generated at docs/dependency-graph.svg" - name: Commit and Push Changes diff --git a/bun.lock b/bun.lock index 5ab66752..c9f06ca7 100644 --- a/bun.lock +++ b/bun.lock @@ -24,6 +24,7 @@ "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", + "dependency-cruiser": "^16.10.0", "knip": "^5.46.0", "typescript": "^5.8.2", "wrap-ansi": "^9.0.0", @@ -110,8 +111,20 @@ "@unhead/schema": ["@unhead/schema@1.11.19", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg=="], + "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "acorn-jsx-walk": ["acorn-jsx-walk@2.0.0", "", {}, "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA=="], + + "acorn-loose": ["acorn-loose@8.4.0", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ=="], + + "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -148,15 +161,15 @@ "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], - "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], - "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], @@ -170,6 +183,8 @@ "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + "dependency-cruiser": ["dependency-cruiser@16.10.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "acorn-jsx-walk": "^2.0.0", "acorn-loose": "^8.4.0", "acorn-walk": "^8.3.4", "ajv": "^8.17.1", "commander": "^13.1.0", "enhanced-resolve": "^5.18.1", "ignore": "^7.0.3", "interpret": "^3.1.1", "is-installed-globally": "^1.0.0", "json5": "^2.2.3", "memoize": "^10.0.0", "picocolors": "^1.1.1", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rechoir": "^0.8.0", "safe-regex": "^2.1.1", "semver": "^7.7.1", "teamcity-service-messages": "^0.1.14", "tsconfig-paths-webpack-plugin": "^4.2.0", "watskeburt": "^4.2.3" }, "bin": { "dependency-cruiser": "bin/dependency-cruise.mjs", "dependency-cruise": "bin/dependency-cruise.mjs", "depcruise": "bin/dependency-cruise.mjs", "depcruise-baseline": "bin/depcruise-baseline.mjs", "depcruise-fmt": "bin/depcruise-fmt.mjs", "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs" } }, "sha512-o6pEB8X/XS0AjpQBhPJW3pSY7HIviRM7+G601T9ruV63NVJC4DxLMA+a1VzZlKOzO2fO6JKRHjRmGjzZZHEFYA=="], + "docker-compose": ["docker-compose@1.1.1", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-UkIUz0LtzuO17Ijm6SXMGtfZMs7IvbNwvuJBiBuN93PIhr/n9/sbJMqpvYFaCBGfwu1ZM4PPPDgQzeeke4lEoA=="], "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], @@ -190,8 +205,12 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], @@ -202,34 +221,52 @@ "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "ignore": ["ignore@7.0.3", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="], "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], + + "interpret": ["interpret@3.1.1", "", {}, "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ=="], + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-installed-globally": ["is-installed-globally@1.0.0", "", { "dependencies": { "global-directory": "^4.0.1", "is-path-inside": "^4.0.0" } }, "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -238,6 +275,12 @@ "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "knip": ["knip@5.46.0", "", { "dependencies": { "@nodelib/fs.walk": "3.0.1", "@snyk/github-codeowners": "1.1.0", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.0", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "summary": "2.1.0", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-WedHSK5xNBWYgm64Rt5B9b0CVXL2kRBcyCeet3NHgdv9en3QE4AWSDPEiX48NoPUBW3h//9S0VwLF5MG/MPi3g=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], @@ -250,10 +293,14 @@ "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], + "memoize": ["memoize@10.1.0", "", { "dependencies": { "mimic-function": "^5.0.1" } }, "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], @@ -278,6 +325,8 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -286,6 +335,8 @@ "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], @@ -296,24 +347,38 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "rechoir": ["rechoir@0.8.0", "", { "dependencies": { "resolve": "^1.20.0" } }, "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ=="], + + "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-regex": ["safe-regex@2.1.1", "", { "dependencies": { "regexp-tree": "~0.1.1" } }, "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="], "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], @@ -330,22 +395,34 @@ "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="], "summary": ["summary@2.1.0", "", {}, "sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], "tar-fs": ["tar-fs@2.0.1", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.0.0" } }, "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "teamcity-service-messages": ["teamcity-service-messages@0.1.14", "", {}, "sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w=="], + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], + + "tsconfig-paths-webpack-plugin": ["tsconfig-paths-webpack-plugin@4.2.0", "", { "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", "tapable": "^2.2.1", "tsconfig-paths": "^4.1.2" } }, "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA=="], + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], @@ -356,6 +433,8 @@ "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "watskeburt": ["watskeburt@4.2.3", "", { "bin": { "watskeburt": "dist/run-cli.js" } }, "sha512-uG9qtQYoHqAsnT711nG5iZc/8M5inSmkGCOp7pFaytKG2aTfIca7p//CjiVzAE4P7hzaYuCozMjNNaLgmhbK5g=="], + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -386,6 +465,10 @@ "@scalar/themes/@scalar/types": ["@scalar/types@0.0.34", "", { "dependencies": { "@scalar/openapi-types": "0.1.8", "@unhead/schema": "^1.11.11" } }, "sha512-q01ctijmHArM5KOny2zU+sHfhpsgOAENrDENecK2TsQNn5FYLmFZouMKeW2M6F7KFLPZnFxUiL/rT88b6Rp/Kg=="], + "@snyk/github-codeowners/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "@snyk/github-codeowners/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "@types/docker-modem/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], "@types/dockerode/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], @@ -404,6 +487,10 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "color-string/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], "fast-glob/@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], @@ -414,6 +501,8 @@ "strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "tsconfig-paths-webpack-plugin/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.8", "", {}, "sha512-iufA5/6hPCmRIVD2eh7qGpoKvoA08Gw/qUb2JECifBtAwA93fo7+1k9uHK440f2LMJsbxIzA+nv7RS0BmfiO/g=="], @@ -424,14 +513,14 @@ "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "fast-glob/@nodelib/fs.walk/@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + "tsconfig-paths-webpack-plugin/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], } } diff --git a/dependency-graph.mmd b/dependency-graph.mmd new file mode 100644 index 0000000000000000000000000000000000000000..4602d0bdc0d7b3d492e30063fae90caf31a09bc7 GIT binary patch literal 2660 zcmb7GU2oG+4D~Y-|6$_2h)`O_3lgw(jFqk*z!->!H0|0#)(jDDXLQ9HiLt8qcK8@)iJ-{cY9ZhM$nwT=i9MKc{N{=Y#Hwiz# z?LkFACG2}c^9I~g;AZT7%bzKK@)Euc?ULW59DUBd9y2cg=dTOBM94T&PAk?*#9tNM zoYpkklj$?~*sQ!H9siOAA#TkGqJmGlBznF3*5`&FITvl_hgSEjFS2AcAI3t!5T^vP#^4vm~X z;dH~#^uK4|9ejv&#`zWfzrb7JNZ9yU=}aG5i6`eNWGmbz?_rT4$Am3pVswg&hyx&G z`>b%bq^2AGzTnuQot=~+Y|C>TcIS|fQH_uCWFE2~!(R7@O!6W81o(1S58L=DxrBAm zHOq!a`EOUi@9SsSug}2y4*QwAuF1Q+t0U4~U*AcK)V<%_H*29=+**{oWYN0YE&86P zZ^~z=*m>2>7LndIzLRC2p3#uAFJPvAzeMgnm9rG{<`C2zQ{uOg?P5fFx61hvtlrJH zjW$i$MWow)%eSVSF}cfpS0mDmU%jELIqBZFjLQCziexuSO=Z3!(mSARQAOyUFC*@- zb*i{6p4k*=I{4WEAjpCy^G literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 55a9e43b..da501770 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", + "dependency-cruiser": "^16.10.0", "knip": "^5.46.0", "typescript": "^5.8.2", "wrap-ansi": "^9.0.0" From bede27673a3dc28205951ef552af15fd91f5ac4c Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:30:49 +0100 Subject: [PATCH 177/369] Fix: Change to Bun run --- dependencies.md | 6 ------ dependency-graph.svg | 13 ------------- 2 files changed, 19 deletions(-) delete mode 100644 dependencies.md delete mode 100644 dependency-graph.svg diff --git a/dependencies.md b/dependencies.md deleted file mode 100644 index fbb7268f..00000000 --- a/dependencies.md +++ /dev/null @@ -1,6 +0,0 @@ -# DockStatAPI Dependency Graph - -```mermaid -graph TD - "index.ts" --> "routes/stacks.ts" -``` diff --git a/dependency-graph.svg b/dependency-graph.svg deleted file mode 100644 index b27cd795..00000000 --- a/dependency-graph.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - -dependency-cruiser output - - - From af85b57fc1db535d85d0fde78e13ea5bd7d996db Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 13 Mar 2025 22:31:34 +0000 Subject: [PATCH 178/369] Update dependency graphs --- dependency-graph.mmd | 87 +++++++ dependency-graph.svg | 559 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 646 insertions(+) create mode 100644 dependency-graph.mmd create mode 100644 dependency-graph.svg diff --git a/dependency-graph.mmd b/dependency-graph.mmd new file mode 100644 index 00000000..2aa02d56 --- /dev/null +++ b/dependency-graph.mmd @@ -0,0 +1,87 @@ +flowchart LR + +subgraph 0["src"] +subgraph 1["core"] +subgraph 2["database"] +3["helper.ts"] +6["repository.ts"] +end +subgraph 4["utils"] +5["logger.ts"] +E["change-me-checker.ts"] +T["calculations.ts"] +U["package-json.ts"] +V["respone-handler.ts"] +end +subgraph 7["docker"] +8["client.ts"] +9["scheduler.ts"] +A["store-container-stats.ts"] +B["store-host-stats.ts"] +end +subgraph C["plugins"] +D["loader.ts"] +F["plugin-manager.ts"] +G["plugin-actions.ts"] +end +subgraph H["stacks"] +I["controller.ts"] +end +subgraph J["trpc"] +K["index.ts"] +L["router.ts"] +subgraph M["procedures"] +N["api-config.procedure.ts"] +P["docker-manager.procedure.ts"] +Q["docker-stats.procedure.ts"] +R["logs.procedure.ts"] +S["stacks.procedure.ts"] +end +O["trpc.ts"] +end +end +W["index.ts"] +subgraph X["routes"] +Y["stacks.ts"] +12["api-config.ts"] +13["docker-manager.ts"] +14["docker-stats.ts"] +15["docker-websocket.ts"] +16["logs.ts"] +end +subgraph Z["plugins"] +10["example.plugin.ts"] +11["telegram.plugin.ts"] +end +subgraph 17["typings"] +18["database.ts"] +19["docker-compose.ts"] +1A["docker.ts"] +1B["dockerode.ts"] +1C["plugin.ts"] +1D["websocket.ts"] +end +end +3-->5 +5-->6 +6-->3 +D-->E +D-->5 +D-->F +F-->5 +G-->F +I-->6 +I-->5 +K-->L +L-->N +L-->P +L-->Q +L-->R +L-->S +L-->O +N-->O +P-->O +Q-->O +R-->O +S-->O +W-->Y diff --git a/dependency-graph.svg b/dependency-graph.svg new file mode 100644 index 00000000..fe7fa830 --- /dev/null +++ b/dependency-graph.svg @@ -0,0 +1,559 @@ + + + + + + +dependency-cruiser output + + +cluster_src + +src + + +cluster_src/core + +core + + +cluster_src/core/database + +database + + +cluster_src/core/docker + +docker + + +cluster_src/core/plugins + +plugins + + +cluster_src/core/stacks + +stacks + + +cluster_src/core/trpc + +trpc + + +cluster_src/core/trpc/procedures + +procedures + + +cluster_src/core/utils + +utils + + +cluster_src/plugins + +plugins + + +cluster_src/routes + +routes + + +cluster_src/typings + +typings + + + +src/core/database/helper.ts + + +helper.ts + + + + + +src/core/utils/logger.ts + + +logger.ts + + + + + +src/core/database/helper.ts->src/core/utils/logger.ts + + + + + + + +src/core/database/repository.ts + + +repository.ts + + + + + +src/core/utils/logger.ts->src/core/database/repository.ts + + + + + + + +src/core/database/repository.ts->src/core/database/helper.ts + + + + + + + +src/core/docker/client.ts + + +client.ts + + + + + +src/core/docker/scheduler.ts + + +scheduler.ts + + + + + +src/core/docker/store-container-stats.ts + + +store-container-stats.ts + + + + + +src/core/docker/store-host-stats.ts + + +store-host-stats.ts + + + + + +src/core/plugins/loader.ts + + +loader.ts + + + + + +src/core/plugins/loader.ts->src/core/utils/logger.ts + + + + + +src/core/utils/change-me-checker.ts + + +change-me-checker.ts + + + + + +src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts + + + + + +src/core/plugins/plugin-manager.ts + + +plugin-manager.ts + + + + + +src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts + + + + + +src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts + + + + + +src/core/plugins/plugin-actions.ts + + +plugin-actions.ts + + + + + +src/core/plugins/plugin-actions.ts->src/core/plugins/plugin-manager.ts + + + + + +src/core/stacks/controller.ts + + +controller.ts + + + + + +src/core/stacks/controller.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/controller.ts->src/core/database/repository.ts + + + + + +src/core/trpc/index.ts + + +index.ts + + + + + +src/core/trpc/router.ts + + +router.ts + + + + + +src/core/trpc/index.ts->src/core/trpc/router.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts + + +api-config.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts + + + + + +src/core/trpc/trpc.ts + + +trpc.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts + + +docker-manager.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts + + +docker-stats.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts + + +logs.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts + + +stacks.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/utils/calculations.ts + + +calculations.ts + + + + + +src/core/utils/package-json.ts + + +package-json.ts + + + + + +src/core/utils/respone-handler.ts + + +respone-handler.ts + + + + + +src/index.ts + + +index.ts + + + + + +src/routes/stacks.ts + + +stacks.ts + + + + + +src/index.ts->src/routes/stacks.ts + + + + + +src/plugins/example.plugin.ts + + +example.plugin.ts + + + + + +src/plugins/telegram.plugin.ts + + +telegram.plugin.ts + + + + + +src/routes/api-config.ts + + +api-config.ts + + + + + +src/routes/docker-manager.ts + + +docker-manager.ts + + + + + +src/routes/docker-stats.ts + + +docker-stats.ts + + + + + +src/routes/docker-websocket.ts + + +docker-websocket.ts + + + + + +src/routes/logs.ts + + +logs.ts + + + + + +src/typings/database.ts + + +database.ts + + + + + +src/typings/docker-compose.ts + + +docker-compose.ts + + + + + +src/typings/docker.ts + + +docker.ts + + + + + +src/typings/dockerode.ts + + +dockerode.ts + + + + + +src/typings/plugin.ts + + +plugin.ts + + + + + +src/typings/websocket.ts + + +websocket.ts + + + + + From b1ff316ce84dac9dc3b3724b29e26f6819161c3a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 13 Mar 2025 23:55:46 +0100 Subject: [PATCH 179/369] Fix: Adjust Workflow to only render svg --- .github/workflows/dependency-graph.yml | 7 +-- dependency-graph.mmd | 87 -------------------------- 2 files changed, 1 insertion(+), 93 deletions(-) delete mode 100644 dependency-graph.mmd diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 86310904..9398d2ba 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -20,14 +20,9 @@ jobs: bun add dependency-cruiser sudo apt-get install -y graphviz - - name: Generate Mermaid Diagram - run: | - bun run depcruise src --no-config --include-only "^src" --output-type mermaid > dependency-graph.mmd - echo "Mermaid diagram generated at dependency-graph.mmd" - - name: Generate SVG Dependency Graph run: | - bun run depcruise src --no-config --include-only "^src" --output-type dot | dot -T svg > dependency-graph.svg + bun run dependency-cruiser --output-type ddot src/index.ts --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json | dot -T svg > dependency-graph.svg echo "SVG graph generated at docs/dependency-graph.svg" - name: Commit and Push Changes diff --git a/dependency-graph.mmd b/dependency-graph.mmd deleted file mode 100644 index 2aa02d56..00000000 --- a/dependency-graph.mmd +++ /dev/null @@ -1,87 +0,0 @@ -flowchart LR - -subgraph 0["src"] -subgraph 1["core"] -subgraph 2["database"] -3["helper.ts"] -6["repository.ts"] -end -subgraph 4["utils"] -5["logger.ts"] -E["change-me-checker.ts"] -T["calculations.ts"] -U["package-json.ts"] -V["respone-handler.ts"] -end -subgraph 7["docker"] -8["client.ts"] -9["scheduler.ts"] -A["store-container-stats.ts"] -B["store-host-stats.ts"] -end -subgraph C["plugins"] -D["loader.ts"] -F["plugin-manager.ts"] -G["plugin-actions.ts"] -end -subgraph H["stacks"] -I["controller.ts"] -end -subgraph J["trpc"] -K["index.ts"] -L["router.ts"] -subgraph M["procedures"] -N["api-config.procedure.ts"] -P["docker-manager.procedure.ts"] -Q["docker-stats.procedure.ts"] -R["logs.procedure.ts"] -S["stacks.procedure.ts"] -end -O["trpc.ts"] -end -end -W["index.ts"] -subgraph X["routes"] -Y["stacks.ts"] -12["api-config.ts"] -13["docker-manager.ts"] -14["docker-stats.ts"] -15["docker-websocket.ts"] -16["logs.ts"] -end -subgraph Z["plugins"] -10["example.plugin.ts"] -11["telegram.plugin.ts"] -end -subgraph 17["typings"] -18["database.ts"] -19["docker-compose.ts"] -1A["docker.ts"] -1B["dockerode.ts"] -1C["plugin.ts"] -1D["websocket.ts"] -end -end -3-->5 -5-->6 -6-->3 -D-->E -D-->5 -D-->F -F-->5 -G-->F -I-->6 -I-->5 -K-->L -L-->N -L-->P -L-->Q -L-->R -L-->S -L-->O -N-->O -P-->O -Q-->O -R-->O -S-->O -W-->Y From 14c4240cd7e7b9e7e706da16741ba9d5790a87ae Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 13 Mar 2025 22:56:11 +0000 Subject: [PATCH 180/369] Update dependency graphs --- dependency-graph.svg | 762 ++++++++++++++++--------------------------- 1 file changed, 284 insertions(+), 478 deletions(-) diff --git a/dependency-graph.svg b/dependency-graph.svg index fe7fa830..144a9bfd 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,556 +4,362 @@ - - + + dependency-cruiser output - + cluster_src - -src + +src cluster_src/core - -core + +core -cluster_src/core/database - -database - - -cluster_src/core/docker - -docker - - -cluster_src/core/plugins - -plugins - - -cluster_src/core/stacks - -stacks - - cluster_src/core/trpc - -trpc - - -cluster_src/core/trpc/procedures - -procedures - - -cluster_src/core/utils - -utils - - -cluster_src/plugins - -plugins - - -cluster_src/routes - -routes - - -cluster_src/typings - -typings - - + +trpc + + -src/core/database/helper.ts - - -helper.ts +. + + + + + +. - + -src/core/utils/logger.ts - - -logger.ts - - - - - -src/core/database/helper.ts->src/core/utils/logger.ts - - - - - - - -src/core/database/repository.ts - - -repository.ts +fs + + + + + +fs - - -src/core/utils/logger.ts->src/core/database/repository.ts - - - - - - - -src/core/database/repository.ts->src/core/database/helper.ts - - - - - - + + -src/core/docker/client.ts - - -client.ts +src/routes + + + + + +routes - + + +src->src/routes + + + + -src/core/docker/scheduler.ts - - -scheduler.ts +src/core/database + + + + + +database - + + +src->src/core/database + + + + -src/core/docker/store-container-stats.ts - - -store-container-stats.ts +src/core/docker + + + + + +docker - - -src/core/docker/store-host-stats.ts - - -store-host-stats.ts - - + + +src->src/core/docker + + - - -src/core/plugins/loader.ts - - -loader.ts + + +src/core/plugins + + + + + +plugins - + -src/core/plugins/loader.ts->src/core/utils/logger.ts - - - - - -src/core/utils/change-me-checker.ts - - -change-me-checker.ts - +src->src/core/plugins + + + + + +src->src/core/trpc + + - - -src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - - - - -src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +src/core/utils + + + + + +utils - - -src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - - - - -src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - - - + + +src->src/core/utils + + + + + +src/routes->. + + + + + +src/routes->src/core/database + + + + + +src/routes->src/core/docker + + + + + +src/routes->src/core/utils + + + + -src/core/plugins/plugin-actions.ts - - -plugin-actions.ts +src/typings + + + + + +typings - - -src/core/plugins/plugin-actions.ts->src/core/plugins/plugin-manager.ts - - + + +src/routes->src/typings + + - + -src/core/stacks/controller.ts - - -controller.ts +src/core/stacks + + + + + +stacks - - -src/core/stacks/controller.ts->src/core/utils/logger.ts - - - - - -src/core/stacks/controller.ts->src/core/database/repository.ts - - - - - -src/core/trpc/index.ts - - -index.ts - + + +src/routes->src/core/stacks + + + + +src/core/database->. + + - - -src/core/trpc/router.ts - - -router.ts - + + +src/core/database->src/core/utils + + + + + + +src/core/database->src/typings + + - + -src/core/trpc/index.ts->src/core/trpc/router.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts - - -api-config.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - - - - -src/core/trpc/trpc.ts - - -trpc.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts - - -docker-manager.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts - - -docker-stats.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts - - -logs.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts - - -stacks.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - +src/core/docker->src/core/database + + - + -src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - +src/core/docker->src/core/utils + + - + -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - +src/core/docker->src/typings + + - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - - - + -src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/utils/calculations.ts - - -calculations.ts - - - - - -src/core/utils/package-json.ts - - -package-json.ts - - - - - -src/core/utils/respone-handler.ts - - -respone-handler.ts - - - - - -src/index.ts - - -index.ts - - - - - -src/routes/stacks.ts - - -stacks.ts - - - - - -src/index.ts->src/routes/stacks.ts - - - - - -src/plugins/example.plugin.ts - - -example.plugin.ts - - - - - -src/plugins/telegram.plugin.ts - - -telegram.plugin.ts - - - - - -src/routes/api-config.ts - - -api-config.ts - - - - - -src/routes/docker-manager.ts - - -docker-manager.ts - - - - - -src/routes/docker-stats.ts - - -docker-stats.ts - - +src/core/plugins->. + + - - -src/routes/docker-websocket.ts - - -docker-websocket.ts - - - - - -src/routes/logs.ts - - -logs.ts - - - - - -src/typings/database.ts - - -database.ts - + + +src/core/plugins->src/core/utils + + + + +src/core/plugins->src/typings + + - - -src/typings/docker-compose.ts - - -docker-compose.ts + + +src/core/trpc/procedures + + + + + +procedures - - -src/typings/docker.ts - - -docker.ts - + + +src/core/trpc->src/core/trpc/procedures + + + + + +src/core/utils->. + + + + + +src/core/utils->fs + + + + + +src/core/utils->src/core/database + + + + + + + + +src/typings->. + + + + + +src/core/stacks->src/core/database + + + + +src/core/stacks->src/core/utils + + - - -src/typings/dockerode.ts - - -dockerode.ts - + + +src/core/stacks->src/typings + + + + +src/core/trpc/procedures->src/core/database + + - - -src/typings/plugin.ts - - -plugin.ts - + + +src/core/trpc/procedures->src/core/docker + + + + +src/core/trpc/procedures->src/core/trpc + + - - -src/typings/websocket.ts - - -websocket.ts - + + +src/core/trpc/procedures->src/core/utils + + + + +src/core/trpc/procedures->src/typings + + + + + +src/core/trpc/procedures->src/core/stacks + + From 5996947afe899ff5de39e4cc338d2e5e91f8693a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 14 Mar 2025 00:06:08 +0100 Subject: [PATCH 181/369] Fix: Adjust CI/CD --- .github/scripts/dep-graph.sh | 14 + .github/workflows/dependency-graph.yml | 16 +- dependency-graph.svg | 365 ------------------------- 3 files changed, 27 insertions(+), 368 deletions(-) create mode 100644 .github/scripts/dep-graph.sh delete mode 100644 dependency-graph.svg diff --git a/.github/scripts/dep-graph.sh b/.github/scripts/dep-graph.sh new file mode 100644 index 00000000..d702dc14 --- /dev/null +++ b/.github/scripts/dep-graph.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +mermaidContent="$(cat dependency-graph.mmd)" + +echo " +--- +config: + flowchart: + defaultRenderer: elk +--- + +$mermaidContent +" > dependency-graph.mmd + diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 9398d2ba..019e039a 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -20,10 +20,20 @@ jobs: bun add dependency-cruiser sudo apt-get install -y graphviz - - name: Generate SVG Dependency Graph + - name: Generate Mermaid Dependency Graph run: | - bun run dependency-cruiser --output-type ddot src/index.ts --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json | dot -T svg > dependency-graph.svg - echo "SVG graph generated at docs/dependency-graph.svg" + bun run dependency-cruiser --output-type mermaid src/index.ts --output-to dependency-graph.mmd --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json + echo "Mermaid graph generated at dependency-graph.mmd" + + - name: Convert to ELK Layout + run: | + bash ./.github/scripts/dep-graph.sh + + - name: Generate Dependency Graph (SVG) + run: | + bun run dependency-cruiser --output-type dot src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json + dot -Tsvg dependency-graph.dot -o dependency-graph.svg + echo "SVG graph generated at dependency-graph.svg" - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 diff --git a/dependency-graph.svg b/dependency-graph.svg deleted file mode 100644 index 144a9bfd..00000000 --- a/dependency-graph.svg +++ /dev/null @@ -1,365 +0,0 @@ - - - - - - -dependency-cruiser output - - -cluster_src - -src - - -cluster_src/core - -core - - -cluster_src/core/trpc - -trpc - - - -. - - - - - -. - - - - - -fs - - - - - -fs - - - - - - -src/routes - - - - - -routes - - - - - -src->src/routes - - - - - -src/core/database - - - - - -database - - - - - -src->src/core/database - - - - - -src/core/docker - - - - - -docker - - - - - -src->src/core/docker - - - - - -src/core/plugins - - - - - -plugins - - - - - -src->src/core/plugins - - - - - - -src->src/core/trpc - - - - - -src/core/utils - - - - - -utils - - - - - -src->src/core/utils - - - - - -src/routes->. - - - - - -src/routes->src/core/database - - - - - -src/routes->src/core/docker - - - - - -src/routes->src/core/utils - - - - - -src/typings - - - - - -typings - - - - - -src/routes->src/typings - - - - - -src/core/stacks - - - - - -stacks - - - - - -src/routes->src/core/stacks - - - - - -src/core/database->. - - - - - -src/core/database->src/core/utils - - - - - - - -src/core/database->src/typings - - - - - -src/core/docker->src/core/database - - - - - -src/core/docker->src/core/utils - - - - - -src/core/docker->src/typings - - - - - -src/core/plugins->. - - - - - -src/core/plugins->src/core/utils - - - - - -src/core/plugins->src/typings - - - - - -src/core/trpc/procedures - - - - - -procedures - - - - - -src/core/trpc->src/core/trpc/procedures - - - - - -src/core/utils->. - - - - - -src/core/utils->fs - - - - - -src/core/utils->src/core/database - - - - - - - - -src/typings->. - - - - - -src/core/stacks->src/core/database - - - - - -src/core/stacks->src/core/utils - - - - - -src/core/stacks->src/typings - - - - - -src/core/trpc/procedures->src/core/database - - - - - -src/core/trpc/procedures->src/core/docker - - - - - -src/core/trpc/procedures->src/core/trpc - - - - - -src/core/trpc/procedures->src/core/utils - - - - - -src/core/trpc/procedures->src/typings - - - - - -src/core/trpc/procedures->src/core/stacks - - - - - From 95e184447d0df4585470900d84ed7c80430757c6 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 13 Mar 2025 23:06:33 +0000 Subject: [PATCH 182/369] Update dependency graphs --- dependency-graph.dot | 159 ++++++ dependency-graph.mmd | 186 +++++++ dependency-graph.svg | 1125 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1470 insertions(+) create mode 100644 dependency-graph.dot create mode 100644 dependency-graph.mmd create mode 100644 dependency-graph.svg diff --git a/dependency-graph.dot b/dependency-graph.dot new file mode 100644 index 00000000..4cf81c64 --- /dev/null +++ b/dependency-graph.dot @@ -0,0 +1,159 @@ +strict digraph "dependency-cruiser output"{ + rankdir="LR" splines="true" overlap="false" nodesep="0.16" ranksep="0.18" fontname="Helvetica-bold" fontsize="9" style="rounded,bold,filled" fillcolor="#ffffff" compound="true" + node [shape="box" style="rounded, filled" height="0.2" color="black" fillcolor="#ffffcc" fontcolor="black" fontname="Helvetica" fontsize="9"] + edge [arrowhead="normal" arrowsize="0.6" penwidth="2.0" color="#00000033" fontname="Helvetica" fontsize="9"] + + "bun:sqlite" [label= tooltip="bun:sqlite" ] + "events" [label= tooltip="events" URL="https://nodejs.org/api/events.html" color="grey" fontcolor="grey"] + "fs" [label= tooltip="fs" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] + subgraph "cluster_fs" {label="fs" "fs/promises" [label= tooltip="promises" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] } + "package.json" [label= tooltip="package.json" URL="package.json" fillcolor="#ffee44"] + "path" [label= tooltip="path" URL="https://nodejs.org/api/path.html" color="grey" fontcolor="grey"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/helper.ts" [label= tooltip="helper.ts" URL="src/core/database/helper.ts" fillcolor="#ddfeff"] } } } + "src/core/database/helper.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/repository.ts" [label= tooltip="repository.ts" URL="src/core/database/repository.ts" fillcolor="#ddfeff"] } } } + "src/core/database/repository.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + "src/core/database/repository.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] + "src/core/database/repository.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/database/repository.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/database/repository.ts" -> "bun:sqlite" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/client.ts" [label= tooltip="client.ts" URL="src/core/docker/client.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/client.ts" -> "src/core/utils/logger.ts" + "src/core/docker/client.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/scheduler.ts" [label= tooltip="scheduler.ts" URL="src/core/docker/scheduler.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/scheduler.ts" -> "src/core/database/repository.ts" + "src/core/docker/scheduler.ts" -> "src/core/docker/store-host-stats.ts" + "src/core/docker/scheduler.ts" -> "src/core/docker/store-container-stats.ts" + "src/core/docker/scheduler.ts" -> "src/core/utils/logger.ts" + "src/core/docker/scheduler.ts" -> "src/typings/database.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-container-stats.ts" [label= tooltip="store-container-stats.ts" URL="src/core/docker/store-container-stats.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/store-container-stats.ts" -> "src/core/database/repository.ts" + "src/core/docker/store-container-stats.ts" -> "src/core/docker/client.ts" + "src/core/docker/store-container-stats.ts" -> "src/core/utils/calculations.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-host-stats.ts" [label= tooltip="store-host-stats.ts" URL="src/core/docker/store-host-stats.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/store-host-stats.ts" -> "src/core/database/repository.ts" + "src/core/docker/store-host-stats.ts" -> "src/core/docker/client.ts" + "src/core/docker/store-host-stats.ts" -> "src/core/utils/logger.ts" + "src/core/docker/store-host-stats.ts" -> "src/typings/docker.ts" + "src/core/docker/store-host-stats.ts" -> "src/typings/dockerode.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/loader.ts" [label= tooltip="loader.ts" URL="src/core/plugins/loader.ts" fillcolor="#ddfeff"] } } } + "src/core/plugins/loader.ts" -> "src/core/utils/change-me-checker.ts" + "src/core/plugins/loader.ts" -> "src/core/utils/logger.ts" + "src/core/plugins/loader.ts" -> "src/core/plugins/plugin-manager.ts" + "src/core/plugins/loader.ts" -> "fs" [style="dashed" penwidth="1.0"] + "src/core/plugins/loader.ts" -> "path" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/plugin-manager.ts" [label= tooltip="plugin-manager.ts" URL="src/core/plugins/plugin-manager.ts" fillcolor="#ddfeff"] } } } + "src/core/plugins/plugin-manager.ts" -> "src/core/utils/logger.ts" + "src/core/plugins/plugin-manager.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/plugins/plugin-manager.ts" -> "src/typings/plugin.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/plugins/plugin-manager.ts" -> "events" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/stacks" {label="stacks" "src/core/stacks/controller.ts" [label= tooltip="controller.ts" URL="src/core/stacks/controller.ts" fillcolor="#ddfeff"] } } } + "src/core/stacks/controller.ts" -> "src/core/database/repository.ts" + "src/core/stacks/controller.ts" -> "src/core/utils/logger.ts" + "src/core/stacks/controller.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/stacks/controller.ts" -> "src/typings/docker-compose.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/index.ts" [label= tooltip="index.ts" URL="src/core/trpc/index.ts" fillcolor="#ddfeff"] } } } + "src/core/trpc/index.ts" -> "src/core/trpc/router.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/api-config.procedure.ts" [label= tooltip="api-config.procedure.ts" URL="src/core/trpc/procedures/api-config.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/logger.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/package-json.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/typings/database.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-manager.procedure.ts" [label= tooltip="docker-manager.procedure.ts" URL="src/core/trpc/procedures/docker-manager.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-stats.procedure.ts" [label= tooltip="docker-stats.procedure.ts" URL="src/core/trpc/procedures/docker-stats.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/docker/client.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/calculations.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/logger.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/logs.procedure.ts" [label= tooltip="logs.procedure.ts" URL="src/core/trpc/procedures/logs.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/stacks.procedure.ts" [label= tooltip="stacks.procedure.ts" URL="src/core/trpc/procedures/stacks.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/stacks/controller.ts" + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/router.ts" [label= tooltip="router.ts" URL="src/core/trpc/router.ts" fillcolor="#ddfeff"] } } } + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/api-config.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-manager.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-stats.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/logs.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/stacks.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/trpc.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/trpc.ts" [label= tooltip="trpc.ts" URL="src/core/trpc/trpc.ts" fillcolor="#ddfeff"] } } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/calculations.ts" [label= tooltip="calculations.ts" URL="src/core/utils/calculations.ts" fillcolor="#ddfeff"] } } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/change-me-checker.ts" [label= tooltip="change-me-checker.ts" URL="src/core/utils/change-me-checker.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/change-me-checker.ts" -> "src/core/utils/logger.ts" + "src/core/utils/change-me-checker.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/logger.ts" [label= tooltip="logger.ts" URL="src/core/utils/logger.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/logger.ts" -> "src/core/database/repository.ts" [arrowhead="normalnoneodot"] + "src/core/utils/logger.ts" -> "path" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/package-json.ts" [label= tooltip="package-json.ts" URL="src/core/utils/package-json.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/package-json.ts" -> "package.json" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/respone-handler.ts" [label= tooltip="respone-handler.ts" URL="src/core/utils/respone-handler.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/respone-handler.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } + "src/index.ts" -> "src/routes/stacks.ts" + "src/index.ts" -> "src/core/database/repository.ts" + "src/index.ts" -> "src/core/docker/scheduler.ts" + "src/index.ts" -> "src/core/plugins/loader.ts" + "src/index.ts" -> "src/core/trpc/index.ts" + "src/index.ts" -> "src/core/utils/logger.ts" + "src/index.ts" -> "src/routes/api-config.ts" + "src/index.ts" -> "src/routes/docker-manager.ts" + "src/index.ts" -> "src/routes/docker-stats.ts" + "src/index.ts" -> "src/routes/docker-websocket.ts" + "src/index.ts" -> "src/routes/logs.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/api-config.ts" [label= tooltip="api-config.ts" URL="src/routes/api-config.ts" fillcolor="#ddfeff"] } } + "src/routes/api-config.ts" -> "src/core/database/repository.ts" + "src/routes/api-config.ts" -> "src/core/utils/logger.ts" + "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" + "src/routes/api-config.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/api-config.ts" -> "src/typings/database.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-manager.ts" [label= tooltip="docker-manager.ts" URL="src/routes/docker-manager.ts" fillcolor="#ddfeff"] } } + "src/routes/docker-manager.ts" -> "src/core/database/repository.ts" + "src/routes/docker-manager.ts" -> "src/core/utils/logger.ts" + "src/routes/docker-manager.ts" -> "src/core/utils/respone-handler.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-stats.ts" [label= tooltip="docker-stats.ts" URL="src/routes/docker-stats.ts" fillcolor="#ddfeff"] } } + "src/routes/docker-stats.ts" -> "src/core/database/repository.ts" + "src/routes/docker-stats.ts" -> "src/core/docker/client.ts" + "src/routes/docker-stats.ts" -> "src/core/utils/calculations.ts" + "src/routes/docker-stats.ts" -> "src/core/utils/logger.ts" + "src/routes/docker-stats.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/docker-stats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/routes/docker-stats.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-websocket.ts" [label= tooltip="docker-websocket.ts" URL="src/routes/docker-websocket.ts" fillcolor="#ddfeff"] } } + "src/routes/docker-websocket.ts" -> "src/core/database/repository.ts" + "src/routes/docker-websocket.ts" -> "src/core/docker/client.ts" + "src/routes/docker-websocket.ts" -> "src/core/utils/calculations.ts" + "src/routes/docker-websocket.ts" -> "src/core/utils/logger.ts" + "src/routes/docker-websocket.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/docker-websocket.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/routes/docker-websocket.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] + "src/routes/docker-websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/logs.ts" [label= tooltip="logs.ts" URL="src/routes/logs.ts" fillcolor="#ddfeff"] } } + "src/routes/logs.ts" -> "src/core/database/repository.ts" + "src/routes/logs.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/stacks.ts" [label= tooltip="stacks.ts" URL="src/routes/stacks.ts" fillcolor="#ddfeff"] } } + "src/routes/stacks.ts" -> "src/core/database/repository.ts" + "src/routes/stacks.ts" -> "src/core/stacks/controller.ts" + "src/routes/stacks.ts" -> "src/core/utils/logger.ts" + "src/routes/stacks.ts" -> "src/core/utils/respone-handler.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/database.ts" [label= tooltip="database.ts" URL="src/typings/database.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker-compose.ts" [label= tooltip="docker-compose.ts" URL="src/typings/docker-compose.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker.ts" [label= tooltip="docker.ts" URL="src/typings/docker.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/dockerode.ts" [label= tooltip="dockerode.ts" URL="src/typings/dockerode.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/plugin.ts" [label= tooltip="plugin.ts" URL="src/typings/plugin.ts" fillcolor="#ddfeff"] } } + "src/typings/plugin.ts" -> "src/typings/docker.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/websocket.ts" [label= tooltip="websocket.ts" URL="src/typings/websocket.ts" fillcolor="#ddfeff"] } } + "src/typings/websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] + "stream" [label= tooltip="stream" URL="https://nodejs.org/api/stream.html" color="grey" fontcolor="grey"] +} diff --git a/dependency-graph.mmd b/dependency-graph.mmd new file mode 100644 index 00000000..90c1aa95 --- /dev/null +++ b/dependency-graph.mmd @@ -0,0 +1,186 @@ + +--- +config: + flowchart: + defaultRenderer: elk +--- + +flowchart LR + +subgraph 0["src"] +1["index.ts"] +subgraph 2["routes"] +3["stacks.ts"] +1A["api-config.ts"] +1B["docker-manager.ts"] +1C["docker-stats.ts"] +1D["docker-websocket.ts"] +1G["logs.ts"] +end +subgraph 4["core"] +subgraph 5["database"] +6["repository.ts"] +8["helper.ts"] +end +subgraph 9["utils"] +A["logger.ts"] +I["respone-handler.ts"] +P["calculations.ts"] +T["change-me-checker.ts"] +14["package-json.ts"] +end +subgraph F["stacks"] +G["controller.ts"] +end +subgraph J["docker"] +K["scheduler.ts"] +L["store-host-stats.ts"] +M["client.ts"] +O["store-container-stats.ts"] +end +subgraph Q["plugins"] +R["loader.ts"] +V["plugin-manager.ts"] +end +subgraph Y["trpc"] +Z["index.ts"] +10["router.ts"] +subgraph 11["procedures"] +12["api-config.procedure.ts"] +16["docker-manager.procedure.ts"] +17["docker-stats.procedure.ts"] +18["logs.procedure.ts"] +19["stacks.procedure.ts"] +end +13["trpc.ts"] +end +end +subgraph C["typings"] +D["database.ts"] +E["docker.ts"] +H["docker-compose.ts"] +N["dockerode.ts"] +X["plugin.ts"] +1F["websocket.ts"] +end +end +7["bun:sqlite"] +B["path"] +subgraph S["fs"] +U["promises"] +end +W["events"] +15["package.json"] +1E["stream"] +1-->3 +1-->6 +1-->K +1-->R +1-->Z +1-->A +1-->1A +1-->1B +1-->1C +1-->1D +1-->1G +3-->6 +3-->G +3-->A +3-->I +6-->8 +6-->A +6-->D +6-->E +6-->7 +8-->A +A-->6 +A-->B +G-->6 +G-->A +G-->D +G-->H +I-->A +K-->6 +K-->L +K-->O +K-->A +K-->D +L-->6 +L-->M +L-->A +L-->E +L-->N +M-->A +M-->E +O-->6 +O-->M +O-->P +R-->T +R-->A +R-->V +R-->S +R-->B +T-->A +T-->U +V-->A +V-->E +V-->X +V-->W +X-->E +Z-->10 +10-->12 +10-->16 +10-->17 +10-->18 +10-->19 +10-->13 +12-->13 +12-->6 +12-->A +12-->14 +12-->D +14-->15 +16-->13 +16-->6 +16-->A +17-->13 +17-->6 +17-->M +17-->P +17-->A +17-->E +17-->N +18-->13 +18-->6 +18-->A +19-->13 +19-->6 +19-->G +19-->A +1A-->6 +1A-->A +1A-->14 +1A-->I +1A-->D +1B-->6 +1B-->A +1B-->I +1C-->6 +1C-->M +1C-->P +1C-->A +1C-->I +1C-->E +1C-->N +1D-->6 +1D-->M +1D-->P +1D-->A +1D-->I +1D-->E +1D-->1F +1D-->1E +1F-->1E +1G-->6 +1G-->A + diff --git a/dependency-graph.svg b/dependency-graph.svg new file mode 100644 index 00000000..5dd8bde3 --- /dev/null +++ b/dependency-graph.svg @@ -0,0 +1,1125 @@ + + + + + + +dependency-cruiser output + + +cluster_fs + +fs + + +cluster_src + +src + + +cluster_src/core + +core + + +cluster_src/core/database + +database + + +cluster_src/core/docker + +docker + + +cluster_src/core/plugins + +plugins + + +cluster_src/core/stacks + +stacks + + +cluster_src/core/trpc + +trpc + + +cluster_src/core/trpc/procedures + +procedures + + +cluster_src/core/utils + +utils + + +cluster_src/routes + +routes + + +cluster_src/typings + +typings + + + +bun:sqlite + + +bun:sqlite + + + + + +events + + +events + + + + + +fs + + +fs + + + + + +fs/promises + + +promises + + + + + +package.json + + +package.json + + + + + +path + + +path + + + + + +src/core/database/helper.ts + + +helper.ts + + + + + +src/core/utils/logger.ts + + +logger.ts + + + + + +src/core/database/helper.ts->src/core/utils/logger.ts + + + + + + + +src/core/utils/logger.ts->path + + + + + +src/core/database/repository.ts + + +repository.ts + + + + + +src/core/utils/logger.ts->src/core/database/repository.ts + + + + + + + +src/core/database/repository.ts->bun:sqlite + + + + + +src/core/database/repository.ts->src/core/database/helper.ts + + + + + + + +src/core/database/repository.ts->src/core/utils/logger.ts + + + + + + + +src/typings/database.ts + + +database.ts + + + + + +src/core/database/repository.ts->src/typings/database.ts + + + + + +src/typings/docker.ts + + +docker.ts + + + + + +src/core/database/repository.ts->src/typings/docker.ts + + + + + +src/core/docker/client.ts + + +client.ts + + + + + +src/core/docker/client.ts->src/core/utils/logger.ts + + + + + +src/core/docker/client.ts->src/typings/docker.ts + + + + + +src/core/docker/scheduler.ts + + +scheduler.ts + + + + + +src/core/docker/scheduler.ts->src/core/utils/logger.ts + + + + + +src/core/docker/scheduler.ts->src/core/database/repository.ts + + + + + +src/core/docker/scheduler.ts->src/typings/database.ts + + + + + +src/core/docker/store-host-stats.ts + + +store-host-stats.ts + + + + + +src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts + + + + + +src/core/docker/store-container-stats.ts + + +store-container-stats.ts + + + + + +src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/utils/logger.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/database/repository.ts + + + + + +src/core/docker/store-host-stats.ts->src/typings/docker.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/docker/client.ts + + + + + +src/typings/dockerode.ts + + +dockerode.ts + + + + + +src/core/docker/store-host-stats.ts->src/typings/dockerode.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/database/repository.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/docker/client.ts + + + + + +src/core/utils/calculations.ts + + +calculations.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts + + + + + +src/core/plugins/loader.ts + + +loader.ts + + + + + +src/core/plugins/loader.ts->fs + + + + + +src/core/plugins/loader.ts->path + + + + + +src/core/plugins/loader.ts->src/core/utils/logger.ts + + + + + +src/core/utils/change-me-checker.ts + + +change-me-checker.ts + + + + + +src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts + + + + + +src/core/plugins/plugin-manager.ts + + +plugin-manager.ts + + + + + +src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts + + + + + +src/core/utils/change-me-checker.ts->fs/promises + + + + + +src/core/utils/change-me-checker.ts->src/core/utils/logger.ts + + + + + +src/core/plugins/plugin-manager.ts->events + + + + + +src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts + + + + + +src/core/plugins/plugin-manager.ts->src/typings/docker.ts + + + + + +src/typings/plugin.ts + + +plugin.ts + + + + + +src/core/plugins/plugin-manager.ts->src/typings/plugin.ts + + + + + +src/typings/plugin.ts->src/typings/docker.ts + + + + + +src/core/stacks/controller.ts + + +controller.ts + + + + + +src/core/stacks/controller.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/controller.ts->src/core/database/repository.ts + + + + + +src/core/stacks/controller.ts->src/typings/database.ts + + + + + +src/typings/docker-compose.ts + + +docker-compose.ts + + + + + +src/core/stacks/controller.ts->src/typings/docker-compose.ts + + + + + +src/core/trpc/index.ts + + +index.ts + + + + + +src/core/trpc/router.ts + + +router.ts + + + + + +src/core/trpc/index.ts->src/core/trpc/router.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts + + +api-config.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts + + + + + +src/core/trpc/trpc.ts + + +trpc.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts + + +docker-manager.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts + + +docker-stats.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts + + +logs.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts + + +stacks.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/utils/package-json.ts + + +package-json.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts + + + + + +src/core/utils/package-json.ts->package.json + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/utils/respone-handler.ts + + +respone-handler.ts + + + + + +src/core/utils/respone-handler.ts->src/core/utils/logger.ts + + + + + +src/index.ts + + +index.ts + + + + + +src/index.ts->src/core/utils/logger.ts + + + + + +src/index.ts->src/core/database/repository.ts + + + + + +src/index.ts->src/core/docker/scheduler.ts + + + + + +src/index.ts->src/core/plugins/loader.ts + + + + + +src/index.ts->src/core/trpc/index.ts + + + + + +src/routes/stacks.ts + + +stacks.ts + + + + + +src/index.ts->src/routes/stacks.ts + + + + + +src/routes/api-config.ts + + +api-config.ts + + + + + +src/index.ts->src/routes/api-config.ts + + + + + +src/routes/docker-manager.ts + + +docker-manager.ts + + + + + +src/index.ts->src/routes/docker-manager.ts + + + + + +src/routes/docker-stats.ts + + +docker-stats.ts + + + + + +src/index.ts->src/routes/docker-stats.ts + + + + + +src/routes/docker-websocket.ts + + +docker-websocket.ts + + + + + +src/index.ts->src/routes/docker-websocket.ts + + + + + +src/routes/logs.ts + + +logs.ts + + + + + +src/index.ts->src/routes/logs.ts + + + + + +src/routes/stacks.ts->src/core/utils/logger.ts + + + + + +src/routes/stacks.ts->src/core/database/repository.ts + + + + + +src/routes/stacks.ts->src/core/stacks/controller.ts + + + + + +src/routes/stacks.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/api-config.ts->src/core/utils/logger.ts + + + + + +src/routes/api-config.ts->src/core/database/repository.ts + + + + + +src/routes/api-config.ts->src/typings/database.ts + + + + + +src/routes/api-config.ts->src/core/utils/package-json.ts + + + + + +src/routes/api-config.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/docker-manager.ts->src/core/utils/logger.ts + + + + + +src/routes/docker-manager.ts->src/core/database/repository.ts + + + + + +src/routes/docker-manager.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/docker-stats.ts->src/core/utils/logger.ts + + + + + +src/routes/docker-stats.ts->src/core/database/repository.ts + + + + + +src/routes/docker-stats.ts->src/typings/docker.ts + + + + + +src/routes/docker-stats.ts->src/core/docker/client.ts + + + + + +src/routes/docker-stats.ts->src/core/utils/calculations.ts + + + + + +src/routes/docker-stats.ts->src/typings/dockerode.ts + + + + + +src/routes/docker-stats.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/docker-websocket.ts->src/core/utils/logger.ts + + + + + +src/routes/docker-websocket.ts->src/core/database/repository.ts + + + + + +src/routes/docker-websocket.ts->src/typings/docker.ts + + + + + +src/routes/docker-websocket.ts->src/core/docker/client.ts + + + + + +src/routes/docker-websocket.ts->src/core/utils/calculations.ts + + + + + +src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts + + + + + +src/typings/websocket.ts + + +websocket.ts + + + + + +src/routes/docker-websocket.ts->src/typings/websocket.ts + + + + + +stream + + +stream + + + + + +src/routes/docker-websocket.ts->stream + + + + + +src/routes/logs.ts->src/core/utils/logger.ts + + + + + +src/routes/logs.ts->src/core/database/repository.ts + + + + + +src/typings/websocket.ts->stream + + + + + From 47a277e4ad73a23fbed4fb50adde57215567df8a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 14 Mar 2025 00:10:59 +0100 Subject: [PATCH 183/369] Fix: Switch to DOTT --- .github/scripts/dep-graph.sh | 3 +-- .github/workflows/dependency-graph.yml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/scripts/dep-graph.sh b/.github/scripts/dep-graph.sh index d702dc14..be7d8731 100644 --- a/.github/scripts/dep-graph.sh +++ b/.github/scripts/dep-graph.sh @@ -2,8 +2,7 @@ mermaidContent="$(cat dependency-graph.mmd)" -echo " ---- +echo "--- config: flowchart: defaultRenderer: elk diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 019e039a..9592a452 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -31,7 +31,7 @@ jobs: - name: Generate Dependency Graph (SVG) run: | - bun run dependency-cruiser --output-type dot src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json + bun run dependency-cruiser --output-type dott src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json dot -Tsvg dependency-graph.dot -o dependency-graph.svg echo "SVG graph generated at dependency-graph.svg" From 89a6d953484ae5e53207b37dbdcbd5a23d4c20fd Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 14 Mar 2025 00:12:10 +0100 Subject: [PATCH 184/369] Fix: I meant archi --- .github/workflows/dependency-graph.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 9592a452..77328767 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -31,7 +31,7 @@ jobs: - name: Generate Dependency Graph (SVG) run: | - bun run dependency-cruiser --output-type dott src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json + bun run dependency-cruiser --output-type archi src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json dot -Tsvg dependency-graph.dot -o dependency-graph.svg echo "SVG graph generated at dependency-graph.svg" From 886098c13ac1af5a15faee7f37a9ae81d26511ca Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 13 Mar 2025 23:12:37 +0000 Subject: [PATCH 185/369] Update dependency graphs --- dependency-graph.dot | 161 +----- dependency-graph.mmd | 1 - dependency-graph.svg | 1150 +++++------------------------------------- 3 files changed, 133 insertions(+), 1179 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 4cf81c64..277a1eb2 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -9,151 +9,22 @@ strict digraph "dependency-cruiser output"{ subgraph "cluster_fs" {label="fs" "fs/promises" [label= tooltip="promises" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] } "package.json" [label= tooltip="package.json" URL="package.json" fillcolor="#ffee44"] "path" [label= tooltip="path" URL="https://nodejs.org/api/path.html" color="grey" fontcolor="grey"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/helper.ts" [label= tooltip="helper.ts" URL="src/core/database/helper.ts" fillcolor="#ddfeff"] } } } - "src/core/database/helper.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/repository.ts" [label= tooltip="repository.ts" URL="src/core/database/repository.ts" fillcolor="#ddfeff"] } } } - "src/core/database/repository.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - "src/core/database/repository.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] - "src/core/database/repository.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/database/repository.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/database/repository.ts" -> "bun:sqlite" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/client.ts" [label= tooltip="client.ts" URL="src/core/docker/client.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/client.ts" -> "src/core/utils/logger.ts" - "src/core/docker/client.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/scheduler.ts" [label= tooltip="scheduler.ts" URL="src/core/docker/scheduler.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/scheduler.ts" -> "src/core/database/repository.ts" - "src/core/docker/scheduler.ts" -> "src/core/docker/store-host-stats.ts" - "src/core/docker/scheduler.ts" -> "src/core/docker/store-container-stats.ts" - "src/core/docker/scheduler.ts" -> "src/core/utils/logger.ts" - "src/core/docker/scheduler.ts" -> "src/typings/database.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-container-stats.ts" [label= tooltip="store-container-stats.ts" URL="src/core/docker/store-container-stats.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/store-container-stats.ts" -> "src/core/database/repository.ts" - "src/core/docker/store-container-stats.ts" -> "src/core/docker/client.ts" - "src/core/docker/store-container-stats.ts" -> "src/core/utils/calculations.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-host-stats.ts" [label= tooltip="store-host-stats.ts" URL="src/core/docker/store-host-stats.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/store-host-stats.ts" -> "src/core/database/repository.ts" - "src/core/docker/store-host-stats.ts" -> "src/core/docker/client.ts" - "src/core/docker/store-host-stats.ts" -> "src/core/utils/logger.ts" - "src/core/docker/store-host-stats.ts" -> "src/typings/docker.ts" - "src/core/docker/store-host-stats.ts" -> "src/typings/dockerode.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/loader.ts" [label= tooltip="loader.ts" URL="src/core/plugins/loader.ts" fillcolor="#ddfeff"] } } } - "src/core/plugins/loader.ts" -> "src/core/utils/change-me-checker.ts" - "src/core/plugins/loader.ts" -> "src/core/utils/logger.ts" - "src/core/plugins/loader.ts" -> "src/core/plugins/plugin-manager.ts" - "src/core/plugins/loader.ts" -> "fs" [style="dashed" penwidth="1.0"] - "src/core/plugins/loader.ts" -> "path" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/plugin-manager.ts" [label= tooltip="plugin-manager.ts" URL="src/core/plugins/plugin-manager.ts" fillcolor="#ddfeff"] } } } - "src/core/plugins/plugin-manager.ts" -> "src/core/utils/logger.ts" - "src/core/plugins/plugin-manager.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/plugins/plugin-manager.ts" -> "src/typings/plugin.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/plugins/plugin-manager.ts" -> "events" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/stacks" {label="stacks" "src/core/stacks/controller.ts" [label= tooltip="controller.ts" URL="src/core/stacks/controller.ts" fillcolor="#ddfeff"] } } } - "src/core/stacks/controller.ts" -> "src/core/database/repository.ts" - "src/core/stacks/controller.ts" -> "src/core/utils/logger.ts" - "src/core/stacks/controller.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/stacks/controller.ts" -> "src/typings/docker-compose.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/index.ts" [label= tooltip="index.ts" URL="src/core/trpc/index.ts" fillcolor="#ddfeff"] } } } - "src/core/trpc/index.ts" -> "src/core/trpc/router.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/api-config.procedure.ts" [label= tooltip="api-config.procedure.ts" URL="src/core/trpc/procedures/api-config.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/database/repository.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/logger.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/package-json.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/typings/database.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-manager.procedure.ts" [label= tooltip="docker-manager.procedure.ts" URL="src/core/trpc/procedures/docker-manager.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/database/repository.ts" - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/utils/logger.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-stats.procedure.ts" [label= tooltip="docker-stats.procedure.ts" URL="src/core/trpc/procedures/docker-stats.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/database/repository.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/docker/client.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/calculations.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/logger.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/logs.procedure.ts" [label= tooltip="logs.procedure.ts" URL="src/core/trpc/procedures/logs.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/database/repository.ts" - "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/utils/logger.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/stacks.procedure.ts" [label= tooltip="stacks.procedure.ts" URL="src/core/trpc/procedures/stacks.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/database/repository.ts" - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/stacks/controller.ts" - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/utils/logger.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/router.ts" [label= tooltip="router.ts" URL="src/core/trpc/router.ts" fillcolor="#ddfeff"] } } } - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/api-config.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-manager.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-stats.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/logs.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/stacks.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/trpc.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/trpc.ts" [label= tooltip="trpc.ts" URL="src/core/trpc/trpc.ts" fillcolor="#ddfeff"] } } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/calculations.ts" [label= tooltip="calculations.ts" URL="src/core/utils/calculations.ts" fillcolor="#ddfeff"] } } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/change-me-checker.ts" [label= tooltip="change-me-checker.ts" URL="src/core/utils/change-me-checker.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/change-me-checker.ts" -> "src/core/utils/logger.ts" - "src/core/utils/change-me-checker.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/logger.ts" [label= tooltip="logger.ts" URL="src/core/utils/logger.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/logger.ts" -> "src/core/database/repository.ts" [arrowhead="normalnoneodot"] - "src/core/utils/logger.ts" -> "path" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/package-json.ts" [label= tooltip="package-json.ts" URL="src/core/utils/package-json.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/package-json.ts" -> "package.json" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/respone-handler.ts" [label= tooltip="respone-handler.ts" URL="src/core/utils/respone-handler.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/respone-handler.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" "src/core" [label= tooltip="core" URL="src/core" shape="box3d"] } + "src/core" -> "src/typings" [arrowhead="onormal" penwidth="1.0"] + "src/core" -> "bun:sqlite" + "src/core" -> "path" [style="dashed" penwidth="1.0"] + "src/core" -> "fs" [style="dashed" penwidth="1.0"] + "src/core" -> "fs/promises" [style="dashed" penwidth="1.0"] + "src/core" -> "events" [style="dashed" penwidth="1.0"] + "src/core" -> "package.json" subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } - "src/index.ts" -> "src/routes/stacks.ts" - "src/index.ts" -> "src/core/database/repository.ts" - "src/index.ts" -> "src/core/docker/scheduler.ts" - "src/index.ts" -> "src/core/plugins/loader.ts" - "src/index.ts" -> "src/core/trpc/index.ts" - "src/index.ts" -> "src/core/utils/logger.ts" - "src/index.ts" -> "src/routes/api-config.ts" - "src/index.ts" -> "src/routes/docker-manager.ts" - "src/index.ts" -> "src/routes/docker-stats.ts" - "src/index.ts" -> "src/routes/docker-websocket.ts" - "src/index.ts" -> "src/routes/logs.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/api-config.ts" [label= tooltip="api-config.ts" URL="src/routes/api-config.ts" fillcolor="#ddfeff"] } } - "src/routes/api-config.ts" -> "src/core/database/repository.ts" - "src/routes/api-config.ts" -> "src/core/utils/logger.ts" - "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" - "src/routes/api-config.ts" -> "src/core/utils/respone-handler.ts" - "src/routes/api-config.ts" -> "src/typings/database.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-manager.ts" [label= tooltip="docker-manager.ts" URL="src/routes/docker-manager.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-manager.ts" -> "src/core/database/repository.ts" - "src/routes/docker-manager.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-manager.ts" -> "src/core/utils/respone-handler.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-stats.ts" [label= tooltip="docker-stats.ts" URL="src/routes/docker-stats.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-stats.ts" -> "src/core/database/repository.ts" - "src/routes/docker-stats.ts" -> "src/core/docker/client.ts" - "src/routes/docker-stats.ts" -> "src/core/utils/calculations.ts" - "src/routes/docker-stats.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-stats.ts" -> "src/core/utils/respone-handler.ts" - "src/routes/docker-stats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/routes/docker-stats.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-websocket.ts" [label= tooltip="docker-websocket.ts" URL="src/routes/docker-websocket.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-websocket.ts" -> "src/core/database/repository.ts" - "src/routes/docker-websocket.ts" -> "src/core/docker/client.ts" - "src/routes/docker-websocket.ts" -> "src/core/utils/calculations.ts" - "src/routes/docker-websocket.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-websocket.ts" -> "src/core/utils/respone-handler.ts" - "src/routes/docker-websocket.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/routes/docker-websocket.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] - "src/routes/docker-websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/logs.ts" [label= tooltip="logs.ts" URL="src/routes/logs.ts" fillcolor="#ddfeff"] } } - "src/routes/logs.ts" -> "src/core/database/repository.ts" - "src/routes/logs.ts" -> "src/core/utils/logger.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/stacks.ts" [label= tooltip="stacks.ts" URL="src/routes/stacks.ts" fillcolor="#ddfeff"] } } - "src/routes/stacks.ts" -> "src/core/database/repository.ts" - "src/routes/stacks.ts" -> "src/core/stacks/controller.ts" - "src/routes/stacks.ts" -> "src/core/utils/logger.ts" - "src/routes/stacks.ts" -> "src/core/utils/respone-handler.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/database.ts" [label= tooltip="database.ts" URL="src/typings/database.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker-compose.ts" [label= tooltip="docker-compose.ts" URL="src/typings/docker-compose.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker.ts" [label= tooltip="docker.ts" URL="src/typings/docker.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/dockerode.ts" [label= tooltip="dockerode.ts" URL="src/typings/dockerode.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/plugin.ts" [label= tooltip="plugin.ts" URL="src/typings/plugin.ts" fillcolor="#ddfeff"] } } - "src/typings/plugin.ts" -> "src/typings/docker.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/websocket.ts" [label= tooltip="websocket.ts" URL="src/typings/websocket.ts" fillcolor="#ddfeff"] } } - "src/typings/websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] + "src/index.ts" -> "src/routes" + "src/index.ts" -> "src/core" + subgraph "cluster_src" {label="src" "src/routes" [label= tooltip="routes" URL="src/routes" shape="box3d"] } + "src/routes" -> "src/core" + "src/routes" -> "src/typings" + "src/routes" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] + subgraph "cluster_src" {label="src" "src/typings" [label= tooltip="typings" URL="src/typings" shape="box3d"] } + "src/typings" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] "stream" [label= tooltip="stream" URL="https://nodejs.org/api/stream.html" color="grey" fontcolor="grey"] } diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 90c1aa95..fe385f81 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -1,4 +1,3 @@ - --- config: flowchart: diff --git a/dependency-graph.svg b/dependency-graph.svg index 5dd8bde3..c833eb62 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,77 +4,27 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src - - -cluster_src/core - -core - - -cluster_src/core/database - -database - - -cluster_src/core/docker - -docker - - -cluster_src/core/plugins - -plugins - - -cluster_src/core/stacks - -stacks - - -cluster_src/core/trpc - -trpc - - -cluster_src/core/trpc/procedures - -procedures - - -cluster_src/core/utils - -utils - - -cluster_src/routes - -routes - - -cluster_src/typings - -typings + +src bun:sqlite - -bun:sqlite + +bun:sqlite @@ -82,8 +32,8 @@ events - -events + +events @@ -91,8 +41,8 @@ fs - -fs + +fs @@ -100,8 +50,8 @@ fs/promises - -promises + +promises @@ -109,8 +59,8 @@ package.json - -package.json + +package.json @@ -118,1008 +68,142 @@ path - -path + +path - + -src/core/database/helper.ts - - -helper.ts +src/core + + + + + +core - - -src/core/utils/logger.ts - - -logger.ts - - - - - -src/core/database/helper.ts->src/core/utils/logger.ts - - - - - - - -src/core/utils/logger.ts->path - - - - - -src/core/database/repository.ts - - -repository.ts - - - - - -src/core/utils/logger.ts->src/core/database/repository.ts - - - - - - - -src/core/database/repository.ts->bun:sqlite - - - - + -src/core/database/repository.ts->src/core/database/helper.ts - - - - - - - -src/core/database/repository.ts->src/core/utils/logger.ts - - - - - - - -src/typings/database.ts - - -database.ts - +src/core->bun:sqlite + + + + +src/core->events + + - + -src/core/database/repository.ts->src/typings/database.ts - - - - - -src/typings/docker.ts - - -docker.ts - +src/core->fs + + - - + -src/core/database/repository.ts->src/typings/docker.ts - - - - - -src/core/docker/client.ts - - -client.ts - - +src/core->fs/promises + + - + -src/core/docker/client.ts->src/core/utils/logger.ts - - - - - -src/core/docker/client.ts->src/typings/docker.ts - - - - - -src/core/docker/scheduler.ts - - -scheduler.ts - - - - - -src/core/docker/scheduler.ts->src/core/utils/logger.ts - - - - - -src/core/docker/scheduler.ts->src/core/database/repository.ts - - - - - -src/core/docker/scheduler.ts->src/typings/database.ts - - - - - -src/core/docker/store-host-stats.ts - - -store-host-stats.ts - - - - - -src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - - - - -src/core/docker/store-container-stats.ts - - -store-container-stats.ts - - - - - -src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - - - - -src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - - - - -src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - - - - -src/core/docker/store-host-stats.ts->src/typings/docker.ts - - - - - -src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - - - - -src/typings/dockerode.ts - - -dockerode.ts - - - - - -src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - - - - -src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - - - - -src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - - - - -src/core/utils/calculations.ts - - -calculations.ts - - - - - -src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - - - - -src/core/plugins/loader.ts - - -loader.ts - - - - - -src/core/plugins/loader.ts->fs - - - - - -src/core/plugins/loader.ts->path - - - - - -src/core/plugins/loader.ts->src/core/utils/logger.ts - - - - - -src/core/utils/change-me-checker.ts - - -change-me-checker.ts - - - - - -src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - - - - -src/core/plugins/plugin-manager.ts - - -plugin-manager.ts - - - - - -src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - - - - -src/core/utils/change-me-checker.ts->fs/promises - - - - - -src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - - - - -src/core/plugins/plugin-manager.ts->events - - - - - -src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - - - - -src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - - - - -src/typings/plugin.ts - - -plugin.ts - - - - - -src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - - - - -src/typings/plugin.ts->src/typings/docker.ts - - - - - -src/core/stacks/controller.ts - - -controller.ts - - - - - -src/core/stacks/controller.ts->src/core/utils/logger.ts - - - - - -src/core/stacks/controller.ts->src/core/database/repository.ts - - - - - -src/core/stacks/controller.ts->src/typings/database.ts - - - - - -src/typings/docker-compose.ts - - -docker-compose.ts - - - - - -src/core/stacks/controller.ts->src/typings/docker-compose.ts - - - - - -src/core/trpc/index.ts - - -index.ts - - - - - -src/core/trpc/router.ts - - -router.ts - - - - - -src/core/trpc/index.ts->src/core/trpc/router.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts - - -api-config.procedure.ts - - +src/core->package.json + + - - -src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - - - - -src/core/trpc/trpc.ts - - -trpc.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts - - -docker-manager.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts - - -docker-stats.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts - - -logs.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts - - -stacks.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + +src/core->path + + - - -src/core/utils/package-json.ts - - -package-json.ts + + +src/typings + + + + + +typings - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - - - - -src/core/utils/package-json.ts->package.json - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + +src/core->src/typings + + - - -src/core/utils/respone-handler.ts - - -respone-handler.ts + + +stream + + +stream - - -src/core/utils/respone-handler.ts->src/core/utils/logger.ts - - + + +src/typings->stream + + - + src/index.ts - - -index.ts - - - - - -src/index.ts->src/core/utils/logger.ts - - - - - -src/index.ts->src/core/database/repository.ts - - - - - -src/index.ts->src/core/docker/scheduler.ts - - - - - -src/index.ts->src/core/plugins/loader.ts - - - - - -src/index.ts->src/core/trpc/index.ts - - - - - -src/routes/stacks.ts - - -stacks.ts + + +index.ts - - -src/index.ts->src/routes/stacks.ts - - - - - -src/routes/api-config.ts - - -api-config.ts - - - - - -src/index.ts->src/routes/api-config.ts - - - - - -src/routes/docker-manager.ts - - -docker-manager.ts - - - - - -src/index.ts->src/routes/docker-manager.ts - - - - - -src/routes/docker-stats.ts - - -docker-stats.ts - - - - - -src/index.ts->src/routes/docker-stats.ts - - - - - -src/routes/docker-websocket.ts - - -docker-websocket.ts - - - - - -src/index.ts->src/routes/docker-websocket.ts - - - - - -src/routes/logs.ts - - -logs.ts - - - - - -src/index.ts->src/routes/logs.ts - - - - - -src/routes/stacks.ts->src/core/utils/logger.ts - - - - - -src/routes/stacks.ts->src/core/database/repository.ts - - - - - -src/routes/stacks.ts->src/core/stacks/controller.ts - - - - - -src/routes/stacks.ts->src/core/utils/respone-handler.ts - - - - - -src/routes/api-config.ts->src/core/utils/logger.ts - - - - - -src/routes/api-config.ts->src/core/database/repository.ts - - - - - -src/routes/api-config.ts->src/typings/database.ts - - - - - -src/routes/api-config.ts->src/core/utils/package-json.ts - - - - - -src/routes/api-config.ts->src/core/utils/respone-handler.ts - - - - - -src/routes/docker-manager.ts->src/core/utils/logger.ts - - - - - -src/routes/docker-manager.ts->src/core/database/repository.ts - - - - - -src/routes/docker-manager.ts->src/core/utils/respone-handler.ts - - - - - -src/routes/docker-stats.ts->src/core/utils/logger.ts - - - - - -src/routes/docker-stats.ts->src/core/database/repository.ts - - - - - -src/routes/docker-stats.ts->src/typings/docker.ts - - - - - -src/routes/docker-stats.ts->src/core/docker/client.ts - - - - - -src/routes/docker-stats.ts->src/core/utils/calculations.ts - - - - - -src/routes/docker-stats.ts->src/typings/dockerode.ts - - - - - -src/routes/docker-stats.ts->src/core/utils/respone-handler.ts - - - - - -src/routes/docker-websocket.ts->src/core/utils/logger.ts - - - - - -src/routes/docker-websocket.ts->src/core/database/repository.ts - - - - - -src/routes/docker-websocket.ts->src/typings/docker.ts - - - - - -src/routes/docker-websocket.ts->src/core/docker/client.ts - - - - - -src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - - - - -src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts - - - - - -src/typings/websocket.ts - - -websocket.ts - - - - - -src/routes/docker-websocket.ts->src/typings/websocket.ts - - + + +src/index.ts->src/core + + - - -stream - - -stream + + +src/routes + + + + + +routes - - -src/routes/docker-websocket.ts->stream - - + + +src/index.ts->src/routes + + - - -src/routes/logs.ts->src/core/utils/logger.ts - - + + +src/routes->src/core + + - - -src/routes/logs.ts->src/core/database/repository.ts - - + + +src/routes->src/typings + + - - -src/typings/websocket.ts->stream - - + + +src/routes->stream + + From 93ef0c92e791b90ec7f1a7a524cf3ed19bde3cec Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 14 Mar 2025 00:19:18 +0100 Subject: [PATCH 186/369] Fix: Ortho --- .github/workflows/dependency-graph.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 77328767..03d23f13 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -31,8 +31,8 @@ jobs: - name: Generate Dependency Graph (SVG) run: | - bun run dependency-cruiser --output-type archi src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json - dot -Tsvg dependency-graph.dot -o dependency-graph.svg + bun run dependency-cruiser --output-type dot src/index.ts --output-to dependency-graph.dot --no-config -x node_modules --ts-pre-compilation-deps --ts-config tsconfig.json + dot -T svg -Gsplines=ortho dependency-graph.dot -o dependency-graph.svg echo "SVG graph generated at dependency-graph.svg" - name: Commit and Push Changes From 538d54b0f8d72c5649984b209d88c6f4d9df627d Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 13 Mar 2025 23:19:43 +0000 Subject: [PATCH 187/369] Update dependency graphs --- dependency-graph.dot | 161 +++++- dependency-graph.svg | 1150 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 1178 insertions(+), 133 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 277a1eb2..4cf81c64 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -9,22 +9,151 @@ strict digraph "dependency-cruiser output"{ subgraph "cluster_fs" {label="fs" "fs/promises" [label= tooltip="promises" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] } "package.json" [label= tooltip="package.json" URL="package.json" fillcolor="#ffee44"] "path" [label= tooltip="path" URL="https://nodejs.org/api/path.html" color="grey" fontcolor="grey"] - subgraph "cluster_src" {label="src" "src/core" [label= tooltip="core" URL="src/core" shape="box3d"] } - "src/core" -> "src/typings" [arrowhead="onormal" penwidth="1.0"] - "src/core" -> "bun:sqlite" - "src/core" -> "path" [style="dashed" penwidth="1.0"] - "src/core" -> "fs" [style="dashed" penwidth="1.0"] - "src/core" -> "fs/promises" [style="dashed" penwidth="1.0"] - "src/core" -> "events" [style="dashed" penwidth="1.0"] - "src/core" -> "package.json" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/helper.ts" [label= tooltip="helper.ts" URL="src/core/database/helper.ts" fillcolor="#ddfeff"] } } } + "src/core/database/helper.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/repository.ts" [label= tooltip="repository.ts" URL="src/core/database/repository.ts" fillcolor="#ddfeff"] } } } + "src/core/database/repository.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + "src/core/database/repository.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] + "src/core/database/repository.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/database/repository.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/database/repository.ts" -> "bun:sqlite" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/client.ts" [label= tooltip="client.ts" URL="src/core/docker/client.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/client.ts" -> "src/core/utils/logger.ts" + "src/core/docker/client.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/scheduler.ts" [label= tooltip="scheduler.ts" URL="src/core/docker/scheduler.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/scheduler.ts" -> "src/core/database/repository.ts" + "src/core/docker/scheduler.ts" -> "src/core/docker/store-host-stats.ts" + "src/core/docker/scheduler.ts" -> "src/core/docker/store-container-stats.ts" + "src/core/docker/scheduler.ts" -> "src/core/utils/logger.ts" + "src/core/docker/scheduler.ts" -> "src/typings/database.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-container-stats.ts" [label= tooltip="store-container-stats.ts" URL="src/core/docker/store-container-stats.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/store-container-stats.ts" -> "src/core/database/repository.ts" + "src/core/docker/store-container-stats.ts" -> "src/core/docker/client.ts" + "src/core/docker/store-container-stats.ts" -> "src/core/utils/calculations.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-host-stats.ts" [label= tooltip="store-host-stats.ts" URL="src/core/docker/store-host-stats.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/store-host-stats.ts" -> "src/core/database/repository.ts" + "src/core/docker/store-host-stats.ts" -> "src/core/docker/client.ts" + "src/core/docker/store-host-stats.ts" -> "src/core/utils/logger.ts" + "src/core/docker/store-host-stats.ts" -> "src/typings/docker.ts" + "src/core/docker/store-host-stats.ts" -> "src/typings/dockerode.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/loader.ts" [label= tooltip="loader.ts" URL="src/core/plugins/loader.ts" fillcolor="#ddfeff"] } } } + "src/core/plugins/loader.ts" -> "src/core/utils/change-me-checker.ts" + "src/core/plugins/loader.ts" -> "src/core/utils/logger.ts" + "src/core/plugins/loader.ts" -> "src/core/plugins/plugin-manager.ts" + "src/core/plugins/loader.ts" -> "fs" [style="dashed" penwidth="1.0"] + "src/core/plugins/loader.ts" -> "path" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/plugin-manager.ts" [label= tooltip="plugin-manager.ts" URL="src/core/plugins/plugin-manager.ts" fillcolor="#ddfeff"] } } } + "src/core/plugins/plugin-manager.ts" -> "src/core/utils/logger.ts" + "src/core/plugins/plugin-manager.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/plugins/plugin-manager.ts" -> "src/typings/plugin.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/plugins/plugin-manager.ts" -> "events" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/stacks" {label="stacks" "src/core/stacks/controller.ts" [label= tooltip="controller.ts" URL="src/core/stacks/controller.ts" fillcolor="#ddfeff"] } } } + "src/core/stacks/controller.ts" -> "src/core/database/repository.ts" + "src/core/stacks/controller.ts" -> "src/core/utils/logger.ts" + "src/core/stacks/controller.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/stacks/controller.ts" -> "src/typings/docker-compose.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/index.ts" [label= tooltip="index.ts" URL="src/core/trpc/index.ts" fillcolor="#ddfeff"] } } } + "src/core/trpc/index.ts" -> "src/core/trpc/router.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/api-config.procedure.ts" [label= tooltip="api-config.procedure.ts" URL="src/core/trpc/procedures/api-config.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/logger.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/package-json.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/typings/database.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-manager.procedure.ts" [label= tooltip="docker-manager.procedure.ts" URL="src/core/trpc/procedures/docker-manager.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-stats.procedure.ts" [label= tooltip="docker-stats.procedure.ts" URL="src/core/trpc/procedures/docker-stats.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/docker/client.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/calculations.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/logger.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/logs.procedure.ts" [label= tooltip="logs.procedure.ts" URL="src/core/trpc/procedures/logs.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/stacks.procedure.ts" [label= tooltip="stacks.procedure.ts" URL="src/core/trpc/procedures/stacks.procedure.ts" fillcolor="#ddfeff"] } } } } + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/trpc/trpc.ts" + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/stacks/controller.ts" + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/router.ts" [label= tooltip="router.ts" URL="src/core/trpc/router.ts" fillcolor="#ddfeff"] } } } + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/api-config.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-manager.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-stats.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/logs.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/procedures/stacks.procedure.ts" + "src/core/trpc/router.ts" -> "src/core/trpc/trpc.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/trpc.ts" [label= tooltip="trpc.ts" URL="src/core/trpc/trpc.ts" fillcolor="#ddfeff"] } } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/calculations.ts" [label= tooltip="calculations.ts" URL="src/core/utils/calculations.ts" fillcolor="#ddfeff"] } } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/change-me-checker.ts" [label= tooltip="change-me-checker.ts" URL="src/core/utils/change-me-checker.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/change-me-checker.ts" -> "src/core/utils/logger.ts" + "src/core/utils/change-me-checker.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/logger.ts" [label= tooltip="logger.ts" URL="src/core/utils/logger.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/logger.ts" -> "src/core/database/repository.ts" [arrowhead="normalnoneodot"] + "src/core/utils/logger.ts" -> "path" [style="dashed" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/package-json.ts" [label= tooltip="package-json.ts" URL="src/core/utils/package-json.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/package-json.ts" -> "package.json" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/respone-handler.ts" [label= tooltip="respone-handler.ts" URL="src/core/utils/respone-handler.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/respone-handler.ts" -> "src/core/utils/logger.ts" subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } - "src/index.ts" -> "src/routes" - "src/index.ts" -> "src/core" - subgraph "cluster_src" {label="src" "src/routes" [label= tooltip="routes" URL="src/routes" shape="box3d"] } - "src/routes" -> "src/core" - "src/routes" -> "src/typings" - "src/routes" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] - subgraph "cluster_src" {label="src" "src/typings" [label= tooltip="typings" URL="src/typings" shape="box3d"] } - "src/typings" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] + "src/index.ts" -> "src/routes/stacks.ts" + "src/index.ts" -> "src/core/database/repository.ts" + "src/index.ts" -> "src/core/docker/scheduler.ts" + "src/index.ts" -> "src/core/plugins/loader.ts" + "src/index.ts" -> "src/core/trpc/index.ts" + "src/index.ts" -> "src/core/utils/logger.ts" + "src/index.ts" -> "src/routes/api-config.ts" + "src/index.ts" -> "src/routes/docker-manager.ts" + "src/index.ts" -> "src/routes/docker-stats.ts" + "src/index.ts" -> "src/routes/docker-websocket.ts" + "src/index.ts" -> "src/routes/logs.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/api-config.ts" [label= tooltip="api-config.ts" URL="src/routes/api-config.ts" fillcolor="#ddfeff"] } } + "src/routes/api-config.ts" -> "src/core/database/repository.ts" + "src/routes/api-config.ts" -> "src/core/utils/logger.ts" + "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" + "src/routes/api-config.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/api-config.ts" -> "src/typings/database.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-manager.ts" [label= tooltip="docker-manager.ts" URL="src/routes/docker-manager.ts" fillcolor="#ddfeff"] } } + "src/routes/docker-manager.ts" -> "src/core/database/repository.ts" + "src/routes/docker-manager.ts" -> "src/core/utils/logger.ts" + "src/routes/docker-manager.ts" -> "src/core/utils/respone-handler.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-stats.ts" [label= tooltip="docker-stats.ts" URL="src/routes/docker-stats.ts" fillcolor="#ddfeff"] } } + "src/routes/docker-stats.ts" -> "src/core/database/repository.ts" + "src/routes/docker-stats.ts" -> "src/core/docker/client.ts" + "src/routes/docker-stats.ts" -> "src/core/utils/calculations.ts" + "src/routes/docker-stats.ts" -> "src/core/utils/logger.ts" + "src/routes/docker-stats.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/docker-stats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/routes/docker-stats.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-websocket.ts" [label= tooltip="docker-websocket.ts" URL="src/routes/docker-websocket.ts" fillcolor="#ddfeff"] } } + "src/routes/docker-websocket.ts" -> "src/core/database/repository.ts" + "src/routes/docker-websocket.ts" -> "src/core/docker/client.ts" + "src/routes/docker-websocket.ts" -> "src/core/utils/calculations.ts" + "src/routes/docker-websocket.ts" -> "src/core/utils/logger.ts" + "src/routes/docker-websocket.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/docker-websocket.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/routes/docker-websocket.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] + "src/routes/docker-websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/logs.ts" [label= tooltip="logs.ts" URL="src/routes/logs.ts" fillcolor="#ddfeff"] } } + "src/routes/logs.ts" -> "src/core/database/repository.ts" + "src/routes/logs.ts" -> "src/core/utils/logger.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/stacks.ts" [label= tooltip="stacks.ts" URL="src/routes/stacks.ts" fillcolor="#ddfeff"] } } + "src/routes/stacks.ts" -> "src/core/database/repository.ts" + "src/routes/stacks.ts" -> "src/core/stacks/controller.ts" + "src/routes/stacks.ts" -> "src/core/utils/logger.ts" + "src/routes/stacks.ts" -> "src/core/utils/respone-handler.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/database.ts" [label= tooltip="database.ts" URL="src/typings/database.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker-compose.ts" [label= tooltip="docker-compose.ts" URL="src/typings/docker-compose.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker.ts" [label= tooltip="docker.ts" URL="src/typings/docker.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/dockerode.ts" [label= tooltip="dockerode.ts" URL="src/typings/dockerode.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/plugin.ts" [label= tooltip="plugin.ts" URL="src/typings/plugin.ts" fillcolor="#ddfeff"] } } + "src/typings/plugin.ts" -> "src/typings/docker.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/websocket.ts" [label= tooltip="websocket.ts" URL="src/typings/websocket.ts" fillcolor="#ddfeff"] } } + "src/typings/websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] "stream" [label= tooltip="stream" URL="https://nodejs.org/api/stream.html" color="grey" fontcolor="grey"] } diff --git a/dependency-graph.svg b/dependency-graph.svg index c833eb62..ada00d65 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,27 +4,77 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src + + +cluster_src/core + +core + + +cluster_src/core/database + +database + + +cluster_src/core/docker + +docker + + +cluster_src/core/plugins + +plugins + + +cluster_src/core/stacks + +stacks + + +cluster_src/core/trpc + +trpc + + +cluster_src/core/trpc/procedures + +procedures + + +cluster_src/core/utils + +utils + + +cluster_src/routes + +routes + + +cluster_src/typings + +typings bun:sqlite - -bun:sqlite + +bun:sqlite @@ -32,8 +82,8 @@ events - -events + +events @@ -41,8 +91,8 @@ fs - -fs + +fs @@ -50,8 +100,8 @@ fs/promises - -promises + +promises @@ -59,8 +109,8 @@ package.json - -package.json + +package.json @@ -68,142 +118,1008 @@ path - -path + +path - + -src/core - - - - - -core +src/core/database/helper.ts + + +helper.ts - - -src/core->bun:sqlite - - + + +src/core/utils/logger.ts + + +logger.ts + + - + + +src/core/database/helper.ts->src/core/utils/logger.ts + + + + + + + +src/core/utils/logger.ts->path + + + + + +src/core/database/repository.ts + + +repository.ts + + + + + +src/core/utils/logger.ts->src/core/database/repository.ts + + + + + + -src/core->events - - +src/core/database/repository.ts->bun:sqlite + + + + + +src/core/database/repository.ts->src/core/database/helper.ts + + + + + + + +src/core/database/repository.ts->src/core/utils/logger.ts + + + + + + + +src/typings/database.ts + + +database.ts + - + + -src/core->fs - - +src/core/database/repository.ts->src/typings/database.ts + + + + + +src/typings/docker.ts + + +docker.ts + - + + -src/core->fs/promises - - +src/core/database/repository.ts->src/typings/docker.ts + + + + + +src/core/docker/client.ts + + +client.ts + + - + -src/core->package.json - - +src/core/docker/client.ts->src/core/utils/logger.ts + + - - -src/core->path - - + + +src/core/docker/client.ts->src/typings/docker.ts + + - - -src/typings - - - - - -typings + + +src/core/docker/scheduler.ts + + +scheduler.ts - - -src/core->src/typings - - + + +src/core/docker/scheduler.ts->src/core/utils/logger.ts + + - - -stream - - -stream + + +src/core/docker/scheduler.ts->src/core/database/repository.ts + + + + + +src/core/docker/scheduler.ts->src/typings/database.ts + + + + + +src/core/docker/store-host-stats.ts + + +store-host-stats.ts - - -src/typings->stream - - + + +src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts + + + + + +src/core/docker/store-container-stats.ts + + +store-container-stats.ts + + + + + +src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/utils/logger.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/database/repository.ts + + + + + +src/core/docker/store-host-stats.ts->src/typings/docker.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/docker/client.ts + + + + + +src/typings/dockerode.ts + + +dockerode.ts + + + + + +src/core/docker/store-host-stats.ts->src/typings/dockerode.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/database/repository.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/docker/client.ts + + + + + +src/core/utils/calculations.ts + + +calculations.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts + + + + + +src/core/plugins/loader.ts + + +loader.ts + + + + + +src/core/plugins/loader.ts->fs + + + + + +src/core/plugins/loader.ts->path + + + + + +src/core/plugins/loader.ts->src/core/utils/logger.ts + + + + + +src/core/utils/change-me-checker.ts + + +change-me-checker.ts + + + + + +src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts + + + + + +src/core/plugins/plugin-manager.ts + + +plugin-manager.ts + + + + + +src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts + + + + + +src/core/utils/change-me-checker.ts->fs/promises + + + + + +src/core/utils/change-me-checker.ts->src/core/utils/logger.ts + + + + + +src/core/plugins/plugin-manager.ts->events + + + + + +src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts + + + + + +src/core/plugins/plugin-manager.ts->src/typings/docker.ts + + + + + +src/typings/plugin.ts + + +plugin.ts + + + + + +src/core/plugins/plugin-manager.ts->src/typings/plugin.ts + + + + + +src/typings/plugin.ts->src/typings/docker.ts + + + + + +src/core/stacks/controller.ts + + +controller.ts + + + + + +src/core/stacks/controller.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/controller.ts->src/core/database/repository.ts + + + + + +src/core/stacks/controller.ts->src/typings/database.ts + + + + + +src/typings/docker-compose.ts + + +docker-compose.ts + + + + + +src/core/stacks/controller.ts->src/typings/docker-compose.ts + + + + + +src/core/trpc/index.ts + + +index.ts + + + + + +src/core/trpc/router.ts + + +router.ts + + + + + +src/core/trpc/index.ts->src/core/trpc/router.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts + + +api-config.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts + + + + + +src/core/trpc/trpc.ts + + +trpc.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts + + +docker-manager.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts + + +docker-stats.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts + + +logs.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts + + +stacks.procedure.ts + + + + + +src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/utils/package-json.ts + + +package-json.ts + + + + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts + + + + + +src/core/utils/package-json.ts->package.json + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts + + + + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts + + + + + +src/core/utils/respone-handler.ts + + +respone-handler.ts + + + + + +src/core/utils/respone-handler.ts->src/core/utils/logger.ts + + - + src/index.ts - - -index.ts + + +index.ts - - -src/index.ts->src/core - - + + +src/index.ts->src/core/utils/logger.ts + + - - -src/routes - - - - - -routes + + +src/index.ts->src/core/database/repository.ts + + + + + +src/index.ts->src/core/docker/scheduler.ts + + + + + +src/index.ts->src/core/plugins/loader.ts + + + + + +src/index.ts->src/core/trpc/index.ts + + + + + +src/routes/stacks.ts + + +stacks.ts - - -src/index.ts->src/routes - - + + +src/index.ts->src/routes/stacks.ts + + - - -src/routes->src/core - - + + +src/routes/api-config.ts + + +api-config.ts + - - -src/routes->src/typings - - - - -src/routes->stream - - + + +src/index.ts->src/routes/api-config.ts + + + + + +src/routes/docker-manager.ts + + +docker-manager.ts + + + + + +src/index.ts->src/routes/docker-manager.ts + + + + + +src/routes/docker-stats.ts + + +docker-stats.ts + + + + + +src/index.ts->src/routes/docker-stats.ts + + + + + +src/routes/docker-websocket.ts + + +docker-websocket.ts + + + + + +src/index.ts->src/routes/docker-websocket.ts + + + + + +src/routes/logs.ts + + +logs.ts + + + + + +src/index.ts->src/routes/logs.ts + + + + + +src/routes/stacks.ts->src/core/utils/logger.ts + + + + + +src/routes/stacks.ts->src/core/database/repository.ts + + + + + +src/routes/stacks.ts->src/core/stacks/controller.ts + + + + + +src/routes/stacks.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/api-config.ts->src/core/utils/logger.ts + + + + + +src/routes/api-config.ts->src/core/database/repository.ts + + + + + +src/routes/api-config.ts->src/typings/database.ts + + + + + +src/routes/api-config.ts->src/core/utils/package-json.ts + + + + + +src/routes/api-config.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/docker-manager.ts->src/core/utils/logger.ts + + + + + +src/routes/docker-manager.ts->src/core/database/repository.ts + + + + + +src/routes/docker-manager.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/docker-stats.ts->src/core/utils/logger.ts + + + + + +src/routes/docker-stats.ts->src/core/database/repository.ts + + + + + +src/routes/docker-stats.ts->src/typings/docker.ts + + + + + +src/routes/docker-stats.ts->src/core/docker/client.ts + + + + + +src/routes/docker-stats.ts->src/core/utils/calculations.ts + + + + + +src/routes/docker-stats.ts->src/typings/dockerode.ts + + + + + +src/routes/docker-stats.ts->src/core/utils/respone-handler.ts + + + + + +src/routes/docker-websocket.ts->src/core/utils/logger.ts + + + + + +src/routes/docker-websocket.ts->src/core/database/repository.ts + + + + + +src/routes/docker-websocket.ts->src/typings/docker.ts + + + + + +src/routes/docker-websocket.ts->src/core/docker/client.ts + + + + + +src/routes/docker-websocket.ts->src/core/utils/calculations.ts + + + + + +src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts + + + + + +src/typings/websocket.ts + + +websocket.ts + + + + + +src/routes/docker-websocket.ts->src/typings/websocket.ts + + + + + +stream + + +stream + + + + + +src/routes/docker-websocket.ts->stream + + + + + +src/routes/logs.ts->src/core/utils/logger.ts + + + + + +src/routes/logs.ts->src/core/database/repository.ts + + + + + +src/typings/websocket.ts->stream + + From 3e851c91e83351c40df633f56da1a018ce687378 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 18 Mar 2025 09:14:01 +0100 Subject: [PATCH 188/369] Fix: Add dependency-graphs to Readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 87b7750f..87fea6f1 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,11 @@ Docker monitoring API with real-time statistics, stack management, and plugin su ## Documentation and Wiki Please see [DockStatAPI](https://dockstatapi.itsnik.de) + +## Project Graph + +### SVG: + +![Dependency Graph](./dependency-graph.svg) + +Click [here](./dependency-graph.mmd) for the mermaid version From 83e52e4ac2670fa9327723249dac88cceaaf7f2a Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 18 Mar 2025 12:31:37 +0100 Subject: [PATCH 189/369] Feat: Authentication, closes #40 and some more adjustments --- src/core/database/repository.ts | 161 ++++++++++-------- .../trpc/procedures/api-config.procedure.ts | 5 +- src/core/utils/respone-handler.ts | 11 +- src/index.ts | 54 +++++- src/middleware/auth.ts | 78 +++++++++ src/routes/api-config.ts | 24 ++- src/typings/database.ts | 1 + src/typings/elysiajs.ts | 12 ++ 8 files changed, 249 insertions(+), 97 deletions(-) create mode 100644 src/middleware/auth.ts create mode 100644 src/typings/elysiajs.ts diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 37f6ba58..d5f11d60 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -2,9 +2,9 @@ import { executeDbOperation } from "./helper"; import Database from "bun:sqlite"; import { logger } from "~/core/utils/logger"; import type { DockerHost, HostStats } from "~/typings/docker"; -import type { stacks_config } from "~/typings/database"; +import type { config, stacks_config } from "~/typings/database"; -const db = new Database("dockstatapi.db"); +const db = new Database("dockstatapi.db", { strict: true }); db.exec("PRAGMA journal_mode = WAL;"); export const dbFunctions = { @@ -13,60 +13,61 @@ export const dbFunctions = { db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - level TEXT, - message TEXT, - file TEXT, - line NUMBER + level TEXT NOT NULL, + message TEXT NOT NULL, + file TEXT NOT NULL, + line NUMBER NOT NULL ); CREATE TABLE IF NOT EXISTS stacks_config ( - name TEXT PRIMARY KEY, - version INTEGER, - custom BOOLEAN, - source TEXT, - container_count INTEGER, - stack_prefix TEXT, - automatic_reboot_on_error BOOLEAN, - image_updates BOOLEAN + name TEXT PRIMARY KEY NOT NULL, + version INTEGER NOT NULL, + custom BOOLEAN NOT NULL, + source TEXT NOT NULL, + container_count INTEGER NOT NULL, + stack_prefix TEXT NOT NULL, + automatic_reboot_on_error BOOLEAN NOT NULL, + image_updates BOOLEAN NOT NULL ); CREATE TABLE IF NOT EXISTS docker_hosts ( - name TEXT, - url TEXT, - secure BOOLEAN + name TEXT NOT NULL, + url TEXT NOT NULL, + secure BOOLEAN NOT NULL ); CREATE TABLE IF NOT EXISTS host_stats ( - hostId TEXT PRIMARY KEY, - dockerVersion TEXT, - apiVersion TEXT, - os TEXT, - architecture TEXT, - totalMemory INTEGER, - totalCPU INTEGER, - labels TEXT, - containers INTEGER, - containersRunning INTEGER, - containersStopped INTEGER, - containersPaused INTEGER, - images INTEGER + hostId TEXT PRIMARY KEY NOT NULL, + dockerVersion TEXT NOT NULL, + apiVersion TEXT NOT NULL, + os TEXT NOT NULL, + architecture TEXT NOT NULL, + totalMemory INTEGER NOT NULL, + totalCPU INTEGER NOT NULL, + labels TEXT NOT NULL, + containers INTEGER NOT NULL, + containersRunning INTEGER NOT NULL, + containersStopped INTEGER NOT NULL, + containersPaused INTEGER NOT NULL, + images INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS container_stats ( - id TEXT, - hostId TEXT, - name TEXT, - image TEXT, - status TEXT, - state TEXT, - cpu_usage FLOAT, - memory_usage FLOAT, + id TEXT NOT NULL, + hostId TEXT NOT NULL, + name TEXT NOT NULL, + image TEXT NOT NULL, + status TEXT NOT NULL, + state TEXT NOT NULL, + cpu_usage FLOAT NOT NULL, + memory_usage FLOAT NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS config ( - keep_data_for NUMBER, - fetching_interval NUMBER + keep_data_for NUMBER NOT NULL, + fetching_interval NUMBER NOT NULL, + api_key TEXT NOT NULL ); `); @@ -76,6 +77,7 @@ export const dbFunctions = { * Default values: * - Data retention value for the database (logs and container stats) 7 days * - Data fetcher for the Database: 5 minutes + * - api_key: changeme */ const configRow = db .prepare(`SELECT COUNT(*) AS count FROM config`) @@ -84,8 +86,8 @@ export const dbFunctions = { logger.debug("Initializing default config"); const stmt = db.prepare( ` - INSERT INTO config (keep_data_for, fetching_interval) VALUES (7, 5) - ` + INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme") + `, ); stmt.run(); } @@ -98,7 +100,7 @@ export const dbFunctions = { const stmt = db.prepare( ` INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) - ` + `, ); stmt.run("Localhost", "localhost:2375", false); } @@ -118,6 +120,20 @@ export const dbFunctions = { return stmt.run(hostId, url, secure); }, () => { + if (hostId.length < 1) { + logger.error("Hostname needed"); + throw new Error( + "Invalid data provided, please see server's log for more info", + ); + } + + if (url.length < 1) { + logger.error("URL needed"); + throw new Error( + "Invalid data provided, please see server's log for more info", + ); + } + if ( typeof hostId !== "string" || typeof url !== "string" || @@ -126,7 +142,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); } - } + }, ); }, @@ -142,7 +158,7 @@ export const dbFunctions = { const data = stmt.all(); return data as DockerHost[]; }, - () => {} + () => {}, ); }, @@ -150,7 +166,7 @@ export const dbFunctions = { level: string, message: string, file_name: string, - line: number + line: number, ) => { if ( typeof level !== "string" || @@ -181,7 +197,7 @@ export const dbFunctions = { const data = stmt.all(); return data; }, - () => {} + () => {}, ); }, @@ -203,7 +219,7 @@ export const dbFunctions = { logger.error("Level parameter must be a string"); throw new TypeError("Level parameter must be a string"); } - } + }, ); }, @@ -228,7 +244,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateDockerHost"); throw new TypeError("Invalid parameter types for updateDockerHost"); } - } + }, ); }, @@ -248,7 +264,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteDockerHost"); throw new TypeError("Name parameter must be a string"); } - } + }, ); }, @@ -262,7 +278,7 @@ export const dbFunctions = { const data = stmt.run(); return data; }, - () => {} + () => {}, ); }, @@ -282,20 +298,25 @@ export const dbFunctions = { logger.error("Invalid parameter type for clearLogsByLevel"); throw new TypeError("Level parameter must be a string"); } - } + }, ); }, - updateConfig(fetching_interval: number, keep_data_for: number) { + updateConfig( + fetching_interval: number, + keep_data_for: number, + api_key: string, + ) { return executeDbOperation( "Update Config", () => { const stmt = db.prepare(` UPDATE config SET fetching_interval = ?, - keep_data_for = ? + keep_data_for = ?, + api_key = ? `); - const data = stmt.run(fetching_interval, keep_data_for); + const data = stmt.run(fetching_interval, keep_data_for, api_key); return data; }, () => { @@ -306,7 +327,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateConfig"); throw new TypeError("Invalid parameter types for updateConfig"); } - } + }, ); }, @@ -315,13 +336,13 @@ export const dbFunctions = { "Get Config", () => { const stmt = db.prepare(` - SELECT keep_data_for, fetching_interval + SELECT keep_data_for, fetching_interval, api_key FROM config `); const data = stmt.all(); return data; }, - () => {} + () => {}, ); }, @@ -346,7 +367,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteOldData"); throw new TypeError("Days parameter must be a number"); } - } + }, ); }, @@ -358,7 +379,7 @@ export const dbFunctions = { status: string, state: string, cpu_usage: number, - memory_usage: number + memory_usage: number, ) { return executeDbOperation( "Add Container Stats", @@ -375,7 +396,7 @@ export const dbFunctions = { status, state, cpu_usage, - memory_usage + memory_usage, ); return data; }, @@ -393,7 +414,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addContainerStats"); throw new TypeError("Invalid parameter types for addContainerStats"); } - } + }, ); }, @@ -446,11 +467,11 @@ export const dbFunctions = { stats.containersRunning, stats.containersStopped, stats.containersPaused, - stats.images + stats.images, ); return data; }, - () => {} + () => {}, ); }, @@ -479,11 +500,11 @@ export const dbFunctions = { stack_config.container_count, stack_config.stack_prefix, stack_config.automatic_reboot_on_error, - stack_config.image_updates + stack_config.image_updates, ); return data; }, - () => {} + () => {}, ); }, @@ -499,7 +520,7 @@ export const dbFunctions = { const data = stmt.all(); return data; }, - () => {} + () => {}, ); }, @@ -514,7 +535,7 @@ export const dbFunctions = { const data = stmt.run(name); return data; }, - () => {} + () => {}, ); }, @@ -542,11 +563,11 @@ export const dbFunctions = { stack_config.stack_prefix, stack_config.automatic_reboot_on_error, stack_config.image_updates, - stack_config.name + stack_config.name, ); return data; }, - () => {} + () => {}, ); }, }; diff --git a/src/core/trpc/procedures/api-config.procedure.ts b/src/core/trpc/procedures/api-config.procedure.ts index bf6cd401..6b3b2488 100644 --- a/src/core/trpc/procedures/api-config.procedure.ts +++ b/src/core/trpc/procedures/api-config.procedure.ts @@ -19,6 +19,7 @@ import { config } from "~/typings/database"; const configInputSchema = z.object({ fetching_interval: z.number(), keep_data_for: z.number(), + api_key: z.string(), }); export const configProcedure = router({ @@ -40,8 +41,8 @@ export const configProcedure = router({ update: publicProcedure.input(configInputSchema).mutation(({ input }) => { try { - const { fetching_interval, keep_data_for } = input; - dbFunctions.updateConfig(fetching_interval, keep_data_for); + const { fetching_interval, keep_data_for, api_key } = input; + dbFunctions.updateConfig(fetching_interval, keep_data_for, api_key); return { success: true, message: "Updated DockStatAPI config" }; } catch (error) { logger.error("tRPC config update error", error); diff --git a/src/core/utils/respone-handler.ts b/src/core/utils/respone-handler.ts index 93e0cdbe..65b7c09f 100644 --- a/src/core/utils/respone-handler.ts +++ b/src/core/utils/respone-handler.ts @@ -1,14 +1,5 @@ import { logger } from "~/core/utils/logger"; -import type { HTTPHeaders } from "elysia/dist/types"; -import type { ElysiaCookie } from "elysia/dist/cookies"; -import type { StatusMap } from "elysia"; - -interface set { - headers: HTTPHeaders; - status?: number | keyof StatusMap; - redirect?: string; - cookie?: Record; -} +import type { set } from "~/typings/elysiajs"; export const responseHandler = { error( diff --git a/src/index.ts b/src/index.ts index 482f924a..7aa47021 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,12 +13,15 @@ import { setSchedules } from "~/core/docker/scheduler"; import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; import trpcRouter from "~/core/trpc"; +import { config } from "./typings/database"; +import { validateApiKey } from "./middleware/auth"; console.log(""); dbFunctions.init(); const DockStatAPI = new Elysia() .use(staticPlugin()) + .use(serverTiming()) .use( swagger({ documentation: { @@ -27,6 +30,21 @@ const DockStatAPI = new Elysia() version: "2.1.0", description: "Docker monitoring API with plugin support", }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey", + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], tags: [ { name: "Statistics", @@ -47,9 +65,24 @@ const DockStatAPI = new Elysia() }, ], }, - }) + }), ) - .use(serverTiming()) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; + + if (path === "/health" || path.startsWith("/swagger")) { + logger.info(`Requested unguarded route: ${path}`); + return; + } + + const validation = await validateApiKey(request, set); + + if (validation.error) { + set.status = 400; + set.headers["Content-Type"] = "application/json"; + return { error: validation.error }; + } + }) .use(trpcRouter) .use(dockerRoutes) .use(dockerStatsRoutes) @@ -58,9 +91,9 @@ const DockStatAPI = new Elysia() .use(apiConfigRoutes) .use(stackRoutes) .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) - .onError(({ code, set }) => { + .onError(({ code, set, path }) => { if (code === "NOT_FOUND") { - logger.warn("Unknown route, showing error page!"); + logger.warn(`Unknown route (${path}), showing error page!`); set.status = 404; set.headers["Content-Type"] = "text/html"; return Bun.file("public/404.html"); @@ -70,14 +103,23 @@ const DockStatAPI = new Elysia() async function startServer() { try { await loadPlugins("./src/plugins"); + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; + + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", + ); + } + DockStatAPI.listen(3000, ({ hostname, port }) => { console.log("----- [ ############## ]"); logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger` + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, ); logger.info( - `tRPC Endpoint available at: http://${hostname}:${port}/trpc` + `tRPC Endpoint available at: http://${hostname}:${port}/trpc`, ); }); } catch (error) { diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts new file mode 100644 index 00000000..ac0c8609 --- /dev/null +++ b/src/middleware/auth.ts @@ -0,0 +1,78 @@ +import { dbFunctions } from "~/core/database/repository"; +import { logger } from "~/core/utils/logger"; +import { config } from "~/typings/database"; +import { set } from "~/typings/elysiajs"; + +export async function hashApiKey(apiKey: string): Promise { + logger.debug("Hashing API key"); + try { + logger.debug("API key hashed successfully"); + return await Bun.password.hash(apiKey); + } catch (error) { + logger.error("Error hashing API key", error); + throw new Error("Failed to hash API key"); + } +} + +async function validateApiKeyHash( + providedKey: string, + storedHash: string, +): Promise { + logger.debug("Validating API key hash"); + try { + const isValid = await Bun.password.verify(providedKey, storedHash); + logger.debug(`API key validation result: ${isValid}`); + return isValid; + } catch (error) { + logger.error("Error validating API key hash", error); + return false; + } +} + +async function getApiKeyFromDb( + apiKey: string, +): Promise<{ hash: string } | null> { + const dbApiKey = (dbFunctions.getConfig() as config[])[0].api_key; + logger.debug(`Querying database for API key: ${apiKey}`); + return Promise.resolve({ + hash: dbApiKey, + }); +} + +export async function validateApiKey(request: Request, set: set) { + const apiKey = request.headers.get("x-api-key"); + logger.debug(`API key validation initiated`); + + if (process.env.NODE_ENV != "production") { + return { apiKey }; + } else if (!apiKey) { + logger.error(`API key missing from request ${request.url}`); + set.status = 401; + return { error: "API key required" }; + } + + try { + const dbRecord = await getApiKeyFromDb(apiKey); + + if (!dbRecord) { + logger.error("API key not found in database"); + set.status = 401; + return { error: "Invalid API key" }; + } + + const isValid = await validateApiKeyHash(apiKey, dbRecord.hash); + + if (!isValid) { + logger.error("Invalid API key provided"); + set.status = 401; + return { error: "Invalid API key" }; + } + + logger.info(`Valid API key used: ${apiKey}`); + return { apiKey }; + } catch (error) { + logger.error("Error during API key validation", error); + set.status = 500; + return { error: "Internal server error" }; + } +} diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index bc081320..1c3b13bf 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -14,10 +14,11 @@ import { devDependencies, license, } from "~/core/utils/package-json"; +import { hashApiKey } from "~/middleware/auth"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) .get( - "/get", + "/", async ({ set }) => { try { const data = dbFunctions.getConfig() as config[]; @@ -30,27 +31,31 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, "Error getting the DockStatAPI config", - error as string + error as string, ); } }, { tags: ["Management"], - } + }, ) .post( "/update", async ({ set, body }) => { try { - const { fetching_interval, keep_data_for } = body; + const { fetching_interval, keep_data_for, api_key } = body; set.headers["Content-Type"] = "application/json"; - dbFunctions.updateConfig(fetching_interval, keep_data_for); + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key), + ); return responseHandler.ok(set, "Updated DockStatAPI config"); } catch (error) { return responseHandler.error( set, "Error updating the DockStatAPI config", - error as string + error as string, ); } }, @@ -58,9 +63,10 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) body: t.Object({ fetching_interval: t.Number(), keep_data_for: t.Number(), + api_key: t.String(), }), tags: ["Management"], - } + }, ) .get( "/package", @@ -82,11 +88,11 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error while reading package.json" + "Error while reading package.json", ); } }, { tags: ["Management"], - } + }, ); diff --git a/src/typings/database.ts b/src/typings/database.ts index c5200e60..c9b15d31 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -9,6 +9,7 @@ interface backend_log_entries { interface config { keep_data_for: number; fetching_interval: number; + api_key: string; } interface stacks_config { diff --git a/src/typings/elysiajs.ts b/src/typings/elysiajs.ts new file mode 100644 index 00000000..913ceea1 --- /dev/null +++ b/src/typings/elysiajs.ts @@ -0,0 +1,12 @@ +import type { StatusMap } from "elysia"; +import type { HTTPHeaders } from "elysia/dist/types"; +import type { ElysiaCookie } from "elysia/dist/cookies"; + +interface set { + headers: HTTPHeaders; + status?: number | keyof StatusMap; + redirect?: string; + cookie?: Record; +} + +export { set }; From c1113fc7056481a1ce32df7cc7105185f4cd063c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 18 Mar 2025 11:39:24 +0000 Subject: [PATCH 190/369] Update dependency graphs --- dependency-graph.dot | 10 + dependency-graph.mmd | 250 +++++++------ dependency-graph.svg | 849 +++++++++++++++++++++++-------------------- 3 files changed, 601 insertions(+), 508 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 4cf81c64..9de730a5 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -100,8 +100,11 @@ strict digraph "dependency-cruiser output"{ "src/core/utils/package-json.ts" -> "package.json" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/respone-handler.ts" [label= tooltip="respone-handler.ts" URL="src/core/utils/respone-handler.ts" fillcolor="#ddfeff"] } } } "src/core/utils/respone-handler.ts" -> "src/core/utils/logger.ts" + "src/core/utils/respone-handler.ts" -> "src/typings/elysiajs.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } + "src/index.ts" -> "src/middleware/auth.ts" "src/index.ts" -> "src/routes/stacks.ts" + "src/index.ts" -> "src/typings/database.ts" "src/index.ts" -> "src/core/database/repository.ts" "src/index.ts" -> "src/core/docker/scheduler.ts" "src/index.ts" -> "src/core/plugins/loader.ts" @@ -112,11 +115,17 @@ strict digraph "dependency-cruiser output"{ "src/index.ts" -> "src/routes/docker-stats.ts" "src/index.ts" -> "src/routes/docker-websocket.ts" "src/index.ts" -> "src/routes/logs.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/middleware" {label="middleware" "src/middleware/auth.ts" [label= tooltip="auth.ts" URL="src/middleware/auth.ts" fillcolor="#ddfeff"] } } + "src/middleware/auth.ts" -> "src/core/database/repository.ts" + "src/middleware/auth.ts" -> "src/core/utils/logger.ts" + "src/middleware/auth.ts" -> "src/typings/database.ts" + "src/middleware/auth.ts" -> "src/typings/elysiajs.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/api-config.ts" [label= tooltip="api-config.ts" URL="src/routes/api-config.ts" fillcolor="#ddfeff"] } } "src/routes/api-config.ts" -> "src/core/database/repository.ts" "src/routes/api-config.ts" -> "src/core/utils/logger.ts" "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" "src/routes/api-config.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/api-config.ts" -> "src/middleware/auth.ts" "src/routes/api-config.ts" -> "src/typings/database.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-manager.ts" [label= tooltip="docker-manager.ts" URL="src/routes/docker-manager.ts" fillcolor="#ddfeff"] } } "src/routes/docker-manager.ts" -> "src/core/database/repository.ts" @@ -151,6 +160,7 @@ strict digraph "dependency-cruiser output"{ subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker-compose.ts" [label= tooltip="docker-compose.ts" URL="src/typings/docker-compose.ts" fillcolor="#ddfeff"] } } subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker.ts" [label= tooltip="docker.ts" URL="src/typings/docker.ts" fillcolor="#ddfeff"] } } subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/dockerode.ts" [label= tooltip="dockerode.ts" URL="src/typings/dockerode.ts" fillcolor="#ddfeff"] } } + subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/elysiajs.ts" [label= tooltip="elysiajs.ts" URL="src/typings/elysiajs.ts" fillcolor="#ddfeff"] } } subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/plugin.ts" [label= tooltip="plugin.ts" URL="src/typings/plugin.ts" fillcolor="#ddfeff"] } } "src/typings/plugin.ts" -> "src/typings/docker.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/websocket.ts" [label= tooltip="websocket.ts" URL="src/typings/websocket.ts" fillcolor="#ddfeff"] } } diff --git a/dependency-graph.mmd b/dependency-graph.mmd index fe385f81..23ac0c92 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -8,13 +8,8 @@ flowchart LR subgraph 0["src"] 1["index.ts"] -subgraph 2["routes"] -3["stacks.ts"] -1A["api-config.ts"] -1B["docker-manager.ts"] -1C["docker-stats.ts"] -1D["docker-websocket.ts"] -1G["logs.ts"] +subgraph 2["middleware"] +3["auth.ts"] end subgraph 4["core"] subgraph 5["database"] @@ -23,69 +18,80 @@ subgraph 5["database"] end subgraph 9["utils"] A["logger.ts"] -I["respone-handler.ts"] -P["calculations.ts"] -T["change-me-checker.ts"] -14["package-json.ts"] +L["respone-handler.ts"] +S["calculations.ts"] +W["change-me-checker.ts"] +17["package-json.ts"] end -subgraph F["stacks"] -G["controller.ts"] +subgraph I["stacks"] +J["controller.ts"] end -subgraph J["docker"] -K["scheduler.ts"] -L["store-host-stats.ts"] -M["client.ts"] -O["store-container-stats.ts"] +subgraph M["docker"] +N["scheduler.ts"] +O["store-host-stats.ts"] +P["client.ts"] +R["store-container-stats.ts"] end -subgraph Q["plugins"] -R["loader.ts"] -V["plugin-manager.ts"] +subgraph T["plugins"] +U["loader.ts"] +Y["plugin-manager.ts"] end -subgraph Y["trpc"] -Z["index.ts"] -10["router.ts"] -subgraph 11["procedures"] -12["api-config.procedure.ts"] -16["docker-manager.procedure.ts"] -17["docker-stats.procedure.ts"] -18["logs.procedure.ts"] -19["stacks.procedure.ts"] +subgraph 11["trpc"] +12["index.ts"] +13["router.ts"] +subgraph 14["procedures"] +15["api-config.procedure.ts"] +19["docker-manager.procedure.ts"] +1A["docker-stats.procedure.ts"] +1B["logs.procedure.ts"] +1C["stacks.procedure.ts"] end -13["trpc.ts"] +16["trpc.ts"] end end subgraph C["typings"] D["database.ts"] E["docker.ts"] -H["docker-compose.ts"] -N["dockerode.ts"] -X["plugin.ts"] -1F["websocket.ts"] +F["elysiajs.ts"] +K["docker-compose.ts"] +Q["dockerode.ts"] +10["plugin.ts"] +1I["websocket.ts"] +end +subgraph G["routes"] +H["stacks.ts"] +1D["api-config.ts"] +1E["docker-manager.ts"] +1F["docker-stats.ts"] +1G["docker-websocket.ts"] +1J["logs.ts"] end end 7["bun:sqlite"] B["path"] -subgraph S["fs"] -U["promises"] +subgraph V["fs"] +X["promises"] end -W["events"] -15["package.json"] -1E["stream"] +Z["events"] +18["package.json"] +1H["stream"] 1-->3 +1-->H +1-->D 1-->6 -1-->K -1-->R -1-->Z +1-->N +1-->U +1-->12 1-->A -1-->1A -1-->1B -1-->1C 1-->1D +1-->1E +1-->1F 1-->1G +1-->1J 3-->6 -3-->G 3-->A -3-->I +3-->D +3-->F 6-->8 6-->A 6-->D @@ -94,92 +100,98 @@ W["events"] 8-->A A-->6 A-->B -G-->6 -G-->A -G-->D -G-->H -I-->A -K-->6 -K-->L -K-->O -K-->A -K-->D -L-->6 -L-->M +H-->6 +H-->J +H-->A +H-->L +J-->6 +J-->A +J-->D +J-->K L-->A -L-->E -L-->N -M-->A -M-->E +L-->F +N-->6 +N-->O +N-->R +N-->A +N-->D O-->6 -O-->M O-->P -R-->T -R-->A -R-->V +O-->A +O-->E +O-->Q +P-->A +P-->E +R-->6 +R-->P R-->S -R-->B -T-->A -T-->U -V-->A -V-->E -V-->X -V-->W -X-->E -Z-->10 -10-->12 -10-->16 -10-->17 -10-->18 -10-->19 -10-->13 +U-->W +U-->A +U-->Y +U-->V +U-->B +W-->A +W-->X +Y-->A +Y-->E +Y-->10 +Y-->Z +10-->E 12-->13 -12-->6 -12-->A -12-->14 -12-->D -14-->15 -16-->13 -16-->6 -16-->A -17-->13 -17-->6 -17-->M -17-->P -17-->A -17-->E -17-->N -18-->13 -18-->6 -18-->A -19-->13 +13-->15 +13-->19 +13-->1A +13-->1B +13-->1C +13-->16 +15-->16 +15-->6 +15-->A +15-->17 +15-->D +17-->18 +19-->16 19-->6 -19-->G 19-->A +1A-->16 1A-->6 +1A-->P +1A-->S 1A-->A -1A-->14 -1A-->I -1A-->D +1A-->E +1A-->Q +1B-->16 1B-->6 1B-->A -1B-->I +1C-->16 1C-->6 -1C-->M -1C-->P +1C-->J 1C-->A -1C-->I -1C-->E -1C-->N 1D-->6 -1D-->M -1D-->P 1D-->A -1D-->I -1D-->E -1D-->1F -1D-->1E -1F-->1E +1D-->17 +1D-->L +1D-->3 +1D-->D +1E-->6 +1E-->A +1E-->L +1F-->6 +1F-->P +1F-->S +1F-->A +1F-->L +1F-->E +1F-->Q 1G-->6 +1G-->P +1G-->S 1G-->A +1G-->L +1G-->E +1G-->1I +1G-->1H +1I-->1H +1J-->6 +1J-->A diff --git a/dependency-graph.svg b/dependency-graph.svg index ada00d65..dcc10989 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,11 +4,11 @@ - - + + dependency-cruiser output - + cluster_fs @@ -16,65 +16,70 @@ cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/trpc - -trpc + +trpc cluster_src/core/trpc/procedures - -procedures + +procedures cluster_src/core/utils - -utils + +utils -cluster_src/routes - -routes +cluster_src/middleware + +middleware +cluster_src/routes + +routes + + cluster_src/typings - -typings + +typings bun:sqlite - -bun:sqlite + +bun:sqlite @@ -109,8 +114,8 @@ package.json - -package.json + +package.json @@ -127,8 +132,8 @@ src/core/database/helper.ts - -helper.ts + +helper.ts @@ -136,394 +141,394 @@ src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/utils/logger.ts->path - - + + src/core/database/repository.ts - -repository.ts + +repository.ts src/core/utils/logger.ts->src/core/database/repository.ts - - - - + + + + src/core/database/repository.ts->bun:sqlite - - + + src/core/database/repository.ts->src/core/database/helper.ts - - - - + + + + src/core/database/repository.ts->src/core/utils/logger.ts - - - - + + + + src/typings/database.ts - -database.ts + +database.ts src/core/database/repository.ts->src/typings/database.ts - - + + src/typings/docker.ts - -docker.ts + +docker.ts src/core/database/repository.ts->src/typings/docker.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->src/typings/docker.ts - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/repository.ts - - + + src/core/docker/scheduler.ts->src/typings/database.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + src/typings/dockerode.ts - -dockerode.ts + +dockerode.ts src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts->fs/promises - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + src/core/plugins/plugin-manager.ts->events - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + src/typings/plugin.ts - -plugin.ts + +plugin.ts src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts->src/core/database/repository.ts - - + + src/core/stacks/controller.ts->src/typings/database.ts - - + + src/typings/docker-compose.ts - -docker-compose.ts + +docker-compose.ts src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + src/core/trpc/index.ts - -index.ts + +index.ts @@ -531,595 +536,661 @@ src/core/trpc/router.ts - -router.ts + +router.ts src/core/trpc/index.ts->src/core/trpc/router.ts - - + + src/core/trpc/procedures/api-config.procedure.ts - -api-config.procedure.ts + +api-config.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - + + src/core/trpc/trpc.ts - -trpc.ts + +trpc.ts src/core/trpc/router.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts - -docker-manager.procedure.ts + +docker-manager.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts - -docker-stats.procedure.ts + +docker-stats.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - + + src/core/trpc/procedures/logs.procedure.ts - -logs.procedure.ts + +logs.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - + + src/core/trpc/procedures/stacks.procedure.ts - -stacks.procedure.ts + +stacks.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - + + src/core/utils/package-json.ts->package.json - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/utils/respone-handler.ts - -respone-handler.ts + +respone-handler.ts src/core/utils/respone-handler.ts->src/core/utils/logger.ts - - + + - + +src/typings/elysiajs.ts + + +elysiajs.ts + + + + + +src/core/utils/respone-handler.ts->src/typings/elysiajs.ts + + + + + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/repository.ts - - + + + + + +src/index.ts->src/typings/database.ts + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/core/trpc/index.ts - - + + + + + +src/middleware/auth.ts + + +auth.ts + + + + + +src/index.ts->src/middleware/auth.ts + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + + + + +src/middleware/auth.ts->src/core/utils/logger.ts + + + + + +src/middleware/auth.ts->src/core/database/repository.ts + + + + + +src/middleware/auth.ts->src/typings/database.ts + + + + + +src/middleware/auth.ts->src/typings/elysiajs.ts + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/repository.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/repository.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/respone-handler.ts - - + + + + + +src/routes/api-config.ts->src/middleware/auth.ts + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->src/core/database/repository.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->src/core/database/repository.ts - - + + - + src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/repository.ts - - + + - + src/routes/docker-websocket.ts->src/typings/docker.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts - - + + - + src/typings/websocket.ts - - -websocket.ts + + +websocket.ts - + src/routes/docker-websocket.ts->src/typings/websocket.ts - - + + - + stream - + stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/repository.ts - - + + - + src/typings/websocket.ts->stream - - + + From df63924b7d12ebe793c51f1435e1a7a9f15a8505 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 18 Mar 2025 22:33:48 +0100 Subject: [PATCH 191/369] Feat: Plugins with their respectful hooks implemented. Fixes #42 --- .local-tests/test-container-changes.sh | 15 +++ src/core/docker/monitor.ts | 136 +++++++++++++++++++++++++ src/core/plugins/plugin-actions.ts | 7 -- src/core/plugins/plugin-manager.ts | 23 ++++- src/core/utils/calculations.ts | 7 ++ src/core/utils/logger.ts | 34 ++++--- src/index.ts | 8 +- src/plugins/example.plugin.ts | 103 ++++++++++++++++--- src/typings/plugin.ts | 6 +- 9 files changed, 296 insertions(+), 43 deletions(-) create mode 100644 .local-tests/test-container-changes.sh create mode 100644 src/core/docker/monitor.ts delete mode 100644 src/core/plugins/plugin-actions.ts diff --git a/.local-tests/test-container-changes.sh b/.local-tests/test-container-changes.sh new file mode 100644 index 00000000..5df50759 --- /dev/null +++ b/.local-tests/test-container-changes.sh @@ -0,0 +1,15 @@ +commands=("kill" "start" "restart" "start" "pause" "unpause") +container="SQLite-web" + +press(){ + echo "Press enter to continue" + read -r -p ">" +} + +for command in "${commands[@]}"; do + press + echo "Running $command for $container" + docker "$command" "$container" +done + +docker start "$container" diff --git a/src/core/docker/monitor.ts b/src/core/docker/monitor.ts new file mode 100644 index 00000000..fe8df259 --- /dev/null +++ b/src/core/docker/monitor.ts @@ -0,0 +1,136 @@ +import type { DockerHost } from "~/typings/docker"; +import { dbFunctions } from "~/core/database/repository"; +import { getDockerClient } from "~/core/docker/client"; +import { logger } from "~/core/utils/logger"; +import { pluginManager } from "../plugins/plugin-manager"; +import { HostStats, ContainerInfo } from "~/typings/docker"; +import { sleep } from "bun"; + +export async function monitorDockerEvents() { + let hosts: DockerHost[]; + + try { + hosts = dbFunctions.getDockerHosts(); + logger.debug( + `Retrieved ${hosts.length} Docker host(s) for event monitoring.`, + ); + } catch (error: unknown) { + logger.error(`Error retrieving Docker hosts: ${(error as Error).message}`); + return; + } + + for (const host of hosts) { + await startFor(host); + } +} + +async function startFor(host: DockerHost) { + const docker = getDockerClient(host); + try { + await docker.ping(); + pluginManager.handleHostReachableAgain(host.name); + } catch (err: any) { + logger.warn(`Restarting Stream for ${host.name} in 10 seconds...`); + pluginManager.handleHostUnreachable(host.name, err); + await sleep(10000); + startFor(host); + } + + try { + const eventsStream = await docker.getEvents(); + logger.debug(`Started events stream for host: ${host.name}`); + + let buffer = ""; + + eventsStream.on("data", (chunk: Buffer) => { + buffer += chunk.toString("utf8"); + const lines = buffer.split(/\r?\n/); + + buffer = lines.pop() || ""; + + for (const line of lines) { + if (line.trim() === "") continue; + + let event: any; + try { + event = JSON.parse(line); + } catch (parseErr: any) { + logger.error( + `Failed to parse event from host ${host.name}: ${parseErr.message}`, + ); + continue; + } + + if (event.Type === "container") { + const containerInfo: ContainerInfo = { + id: event.Actor?.ID || event.id || "", + hostId: host.name, + name: event.Actor?.Attributes?.name || "", + image: event.Actor?.Attributes?.image || event.from || "", + status: event.status || event.Actor?.Attributes?.status || "", + state: event.Actor?.Attributes?.state || event.Action || "", + cpuUsage: 0, + memoryUsage: 0, + }; + + const action = event.Action; + logger.debug(`Triggering Action [${action}]`); + switch (action) { + case "stop": + pluginManager.handleContainerStop(containerInfo); + break; + case "start": + pluginManager.handleContainerStart(containerInfo); + break; + case "die": + pluginManager.handleContainerDie(containerInfo); + break; + case "kill": + pluginManager.handleContainerKill(containerInfo); + break; + case "create": + pluginManager.handleContainerCreate(containerInfo); + break; + case "destroy": + pluginManager.handleContainerDestroy(containerInfo); + break; + case "pause": + pluginManager.handleContainerPause(containerInfo); + break; + case "unpause": + pluginManager.handleContainerUnpause(containerInfo); + break; + case "restart": + pluginManager.handleContainerRestart(containerInfo); + break; + case "update": + pluginManager.handleContainerUpdate(containerInfo); + break; + case "health_status": + pluginManager.handleContainerHealthStatus(containerInfo); + break; + default: + logger.debug( + `Unhandled container event "${action}" on host ${host.name}`, + ); + } + } + } + }); + + eventsStream.on("error", async (err: Error) => { + logger.error(`Events stream error for host ${host.name}: ${err.message}`); + logger.warn(`Restarting Stream for ${host.name} in 10 seconds...`); + await sleep(10000); + startFor(host); + }); + + eventsStream.on("end", () => { + logger.info(`Events stream ended for host ${host.name}`); + }); + } catch (streamErr: any) { + logger.error( + `Failed to start events stream for host ${host.name}: ${streamErr.message}`, + ); + } +} diff --git a/src/core/plugins/plugin-actions.ts b/src/core/plugins/plugin-actions.ts deleted file mode 100644 index f914681d..00000000 --- a/src/core/plugins/plugin-actions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { pluginManager } from "./plugin-manager"; - -export const pluginAction = { - containerStart(containerInfo: any) { - pluginManager.handleContainerStart(containerInfo); - }, -}; diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index a81aa0ea..c030b091 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -2,6 +2,7 @@ import { EventEmitter } from "events"; import { logger } from "../utils/logger"; import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo, HostStats } from "~/typings/docker"; +import { plugin } from "bun"; export class PluginManager extends EventEmitter { private plugins: Map = new Map(); @@ -12,7 +13,7 @@ export class PluginManager extends EventEmitter { logger.debug(`Registered plugin: ${plugin.name}`); } catch (error) { logger.error( - `Registering plugin ${plugin.name} failed: ${error as string}` + `Registering plugin ${plugin.name} failed: ${error as string}`, ); } } @@ -88,15 +89,27 @@ export class PluginManager extends EventEmitter { }); } - handleHostUnreachable(HostStats: HostStats) { + handleHostUnreachable(host: string, err: string) { this.plugins.forEach((plugin) => { - plugin.onHostUnreachable?.(HostStats); + plugin.onHostUnreachable?.(host, err); }); } - handleHostReachableAgain(HostStats: HostStats) { + handleHostReachableAgain(host: string) { this.plugins.forEach((plugin) => { - plugin.onHostReachableAgain?.(HostStats); + plugin.onHostReachableAgain?.(host); + }); + } + + handleContainerKill(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.onContainerKill?.(containerInfo); + }); + } + + handleContainerDie(containerInfo: ContainerInfo) { + this.plugins.forEach((plugin) => { + plugin.handleContainerDie?.(containerInfo); }); } } diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index 3ead3a6d..3d7a81d9 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -1,6 +1,10 @@ import type Docker from "dockerode"; const calculateCpuPercent = (stats: Docker.ContainerStats): number => { + if (stats == null) { + return 0.0; + } + const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage; @@ -10,6 +14,9 @@ const calculateCpuPercent = (stats: Docker.ContainerStats): number => { }; const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { + if (stats == null) { + return 0.0; + } return (stats.memory_stats.usage / stats.memory_stats.limit) * 100; }; diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index b35aeeae..8c321ada 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -14,7 +14,10 @@ const fileLineFormat = format((info) => { for (let i = 2; i < stack.length; i++) { const line = stack[i].trim(); // Exclude lines from node_modules or the current file - if (!line.includes("node_modules") && !line.includes(path.basename(__filename))) { + if ( + !line.includes("node_modules") && + !line.includes(path.basename(__filename)) + ) { const matches = line.match(/\(?(.+):(\d+):(\d+)\)?$/); if (matches) { info.file = path.basename(matches[1]); @@ -49,12 +52,11 @@ const formatTerminalMessage = (message: string, prefixLength: number) => { }; export const logger = createLogger({ - level: process.env.LOG_LEVEL || 'debug', + level: process.env.LOG_LEVEL || "debug", format: format.combine( format.timestamp({ format: "DD/MM HH:mm:ss" }), fileLineFormat(), format.printf(({ timestamp, level, message, file, line }) => { - const levelColors: Record = { error: chalk.red.bold, warn: chalk.yellow.bold, @@ -62,18 +64,22 @@ export const logger = createLogger({ debug: chalk.blue.bold, verbose: chalk.cyan.bold, silly: chalk.magenta.bold, - task: chalk.cyan.bold + task: chalk.cyan.bold, }; if ((message as string).startsWith("__task__")) { message = (message as string).replaceAll("__task__", "").trimStart(); - level = "task" + level = "task"; if ((message as string).startsWith("__db__")) { message = (message as string).replaceAll("__db__", "").trimStart(); - message = `${chalk.magenta("DB")} ${message}` + message = `${chalk.magenta("DB")} ${message}`; } } + if ((file as string).includes("plugin.ts")) { + message = `[ ${chalk.greenBright("Plugin")} ] ${message}`; + } + const paddedLevel = level.toUpperCase().padEnd(5); const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); const coloredContext = chalk.cyan(`${file as string}:${line as number}`); @@ -81,7 +87,7 @@ export const logger = createLogger({ if (process.env.NODE_ENV !== "dev") { return `${coloredLevel} [ ${coloredTimestamp} ] - ${chalk.gray( - message + message, )} - [ ${coloredContext} ]`; } @@ -89,25 +95,25 @@ export const logger = createLogger({ const prefixLength = prefix.length; const formattedMessage = formatTerminalMessage( message as string, - prefixLength + prefixLength, ); const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; try { dbFunctions.addLogEntry( - (level as string).replace(ansiRegex, ''), - (message as string).replace(ansiRegex, ''), - (file as string).replace(ansiRegex, ''), - line as number + (level as string).replace(ansiRegex, ""), + (message as string).replace(ansiRegex, ""), + (file as string).replace(ansiRegex, ""), + line as number, ); } catch (error) { // Use console.error to avoid recursive logging console.error(`Error inserting log into DB: ${String(error)}`); - process.abort() + process.abort(); } return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; - }) + }), ), transports: [new transports.Console()], }); diff --git a/src/index.ts b/src/index.ts index 7aa47021..ff6b763e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import staticPlugin from "@elysiajs/static"; import trpcRouter from "~/core/trpc"; import { config } from "./typings/database"; import { validateApiKey } from "./middleware/auth"; +import { monitorDockerEvents } from "./core/docker/monitor"; console.log(""); dbFunctions.init(); @@ -103,6 +104,12 @@ const DockStatAPI = new Elysia() async function startServer() { try { await loadPlugins("./src/plugins"); + await setSchedules(); + + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); + const configData = dbFunctions.getConfig() as config[]; const apiKey = configData[0].api_key; @@ -128,7 +135,6 @@ async function startServer() { } } -await setSchedules(); await startServer(); logger.info("Started server"); diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index bd71becc..d1a1ec61 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -1,22 +1,97 @@ import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo } from "~/typings/docker"; -import type { HostStats } from "~/typings/docker"; +import { logger } from "~/core/utils/logger"; + +// See https://outline.itsnik.de/s/dockstat/doc/plugin-development-3UBj9gNMKF for more info const ExamplePlugin: Plugin = { name: "Example Plugin", - async onContainerStart(containerInfo: ContainerInfo) {}, - async onContainerStop(containerInfo: ContainerInfo) {}, - async onContainerExit(containerInfo: ContainerInfo) {}, - async onContainerCreate(containerInfo: ContainerInfo) {}, - async onContainerDestroy(containerInfo: ContainerInfo) {}, - async onContainerPause(containerInfo: ContainerInfo) {}, - async onContainerUnpause(containerInfo: ContainerInfo) {}, - async onContainerRestart(containerInfo: ContainerInfo) {}, - async onContainerUpdate(containerInfo: ContainerInfo) {}, - async onContainerRename(containerInfo: ContainerInfo) {}, - async onContainerHealthStatus(containerInfo: ContainerInfo) {}, - async onHostUnreachable(HostStats: HostStats) {}, - async onHostReachableAgain(HostStats: HostStats) {}, + + async onContainerStart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} started on ${containerInfo.hostId}`, + ); + }, + + async onContainerStop(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} stopped on ${containerInfo.hostId}`, + ); + }, + + async onContainerExit(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} exited on ${containerInfo.hostId}`, + ); + }, + + async onContainerCreate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} created on ${containerInfo.hostId}`, + ); + }, + + async onContainerDestroy(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}`, + ); + }, + + async onContainerPause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} pause on ${containerInfo.hostId}`, + ); + }, + + async onContainerUnpause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} resumed on ${containerInfo.hostId}`, + ); + }, + + async onContainerRestart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} restarted on ${containerInfo.hostId}`, + ); + }, + + async onContainerUpdate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} updated on ${containerInfo.hostId}`, + ); + }, + + async onContainerRename(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} renamed on ${containerInfo.hostId}`, + ); + }, + + async onContainerHealthStatus(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} changed status to ${containerInfo.status}`, + ); + }, + + async onHostUnreachable(host: string, err: string) { + logger.info(`Server ${host} unreachable - ${err}`); + }, + + async onHostReachableAgain(host: string) { + logger.info(`Server ${host} reachable`); + }, + + async handleContainerDie(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} died on ${containerInfo.hostId}`, + ); + }, + + async onContainerKill(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} killed on ${containerInfo.hostId}`, + ); + }, } satisfies Plugin; export default ExamplePlugin; diff --git a/src/typings/plugin.ts b/src/typings/plugin.ts index 9994ea67..ee16559c 100644 --- a/src/typings/plugin.ts +++ b/src/typings/plugin.ts @@ -9,6 +9,8 @@ interface Plugin { onContainerStop?: (containerInfo: ContainerInfo) => void; onContainerExit?: (containerInfo: ContainerInfo) => void; onContainerCreate?: (containerInfo: ContainerInfo) => void; + onContainerKill?: (ContainerInfo: ContainerInfo) => void; + handleContainerDie?: (ContainerInfo: ContainerInfo) => void; onContainerDestroy?: (containerInfo: ContainerInfo) => void; onContainerPause?: (containerInfo: ContainerInfo) => void; onContainerUnpause?: (containerInfo: ContainerInfo) => void; @@ -18,8 +20,8 @@ interface Plugin { onContainerHealthStatus?: (containerInfo: ContainerInfo) => void; // Host lifecycle hooks - onHostUnreachable?: (HostStats: HostStats) => void; - onHostReachableAgain?: (HostStats: HostStats) => void; + onHostUnreachable?: (host: string, err: string) => void; + onHostReachableAgain?: (host: string) => void; } export type { Plugin }; From 98854042f724b4a12f3005f30a71c0afed7fc0f1 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 18 Mar 2025 21:35:31 +0000 Subject: [PATCH 192/369] Update dependency graphs --- dependency-graph.dot | 11 + dependency-graph.mmd | 315 +++++------ dependency-graph.svg | 1242 ++++++++++++++++++++++-------------------- 3 files changed, 828 insertions(+), 740 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 9de730a5..06b5870f 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -3,6 +3,7 @@ strict digraph "dependency-cruiser output"{ node [shape="box" style="rounded, filled" height="0.2" color="black" fillcolor="#ffffcc" fontcolor="black" fontname="Helvetica" fontsize="9"] edge [arrowhead="normal" arrowsize="0.6" penwidth="2.0" color="#00000033" fontname="Helvetica" fontsize="9"] + "bun" [label= tooltip="bun" ] "bun:sqlite" [label= tooltip="bun:sqlite" ] "events" [label= tooltip="events" URL="https://nodejs.org/api/events.html" color="grey" fontcolor="grey"] "fs" [label= tooltip="fs" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] @@ -20,6 +21,14 @@ strict digraph "dependency-cruiser output"{ subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/client.ts" [label= tooltip="client.ts" URL="src/core/docker/client.ts" fillcolor="#ddfeff"] } } } "src/core/docker/client.ts" -> "src/core/utils/logger.ts" "src/core/docker/client.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/monitor.ts" [label= tooltip="monitor.ts" URL="src/core/docker/monitor.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/monitor.ts" -> "src/core/plugins/plugin-manager.ts" + "src/core/docker/monitor.ts" -> "src/core/database/repository.ts" + "src/core/docker/monitor.ts" -> "src/core/docker/client.ts" + "src/core/docker/monitor.ts" -> "src/core/utils/logger.ts" + "src/core/docker/monitor.ts" -> "src/typings/docker.ts" + "src/core/docker/monitor.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/docker/monitor.ts" -> "bun" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/scheduler.ts" [label= tooltip="scheduler.ts" URL="src/core/docker/scheduler.ts" fillcolor="#ddfeff"] } } } "src/core/docker/scheduler.ts" -> "src/core/database/repository.ts" "src/core/docker/scheduler.ts" -> "src/core/docker/store-host-stats.ts" @@ -46,6 +55,7 @@ strict digraph "dependency-cruiser output"{ "src/core/plugins/plugin-manager.ts" -> "src/core/utils/logger.ts" "src/core/plugins/plugin-manager.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] "src/core/plugins/plugin-manager.ts" -> "src/typings/plugin.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/plugins/plugin-manager.ts" -> "bun" "src/core/plugins/plugin-manager.ts" -> "events" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/stacks" {label="stacks" "src/core/stacks/controller.ts" [label= tooltip="controller.ts" URL="src/core/stacks/controller.ts" fillcolor="#ddfeff"] } } } "src/core/stacks/controller.ts" -> "src/core/database/repository.ts" @@ -102,6 +112,7 @@ strict digraph "dependency-cruiser output"{ "src/core/utils/respone-handler.ts" -> "src/core/utils/logger.ts" "src/core/utils/respone-handler.ts" -> "src/typings/elysiajs.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } + "src/index.ts" -> "src/core/docker/monitor.ts" "src/index.ts" -> "src/middleware/auth.ts" "src/index.ts" -> "src/routes/stacks.ts" "src/index.ts" -> "src/typings/database.ts" diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 23ac0c92..36b5db96 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -8,190 +8,201 @@ flowchart LR subgraph 0["src"] 1["index.ts"] -subgraph 2["middleware"] -3["auth.ts"] +subgraph 2["core"] +subgraph 3["docker"] +4["monitor.ts"] +K["client.ts"] +U["scheduler.ts"] +V["store-host-stats.ts"] +X["store-container-stats.ts"] end -subgraph 4["core"] -subgraph 5["database"] -6["repository.ts"] -8["helper.ts"] +subgraph 6["plugins"] +7["plugin-manager.ts"] +Z["loader.ts"] end subgraph 9["utils"] A["logger.ts"] -L["respone-handler.ts"] -S["calculations.ts"] -W["change-me-checker.ts"] -17["package-json.ts"] +T["respone-handler.ts"] +Y["calculations.ts"] +11["change-me-checker.ts"] +19["package-json.ts"] end -subgraph I["stacks"] -J["controller.ts"] +subgraph C["database"] +D["repository.ts"] +F["helper.ts"] end -subgraph M["docker"] -N["scheduler.ts"] -O["store-host-stats.ts"] -P["client.ts"] -R["store-container-stats.ts"] +subgraph Q["stacks"] +R["controller.ts"] end -subgraph T["plugins"] -U["loader.ts"] -Y["plugin-manager.ts"] +subgraph 13["trpc"] +14["index.ts"] +15["router.ts"] +subgraph 16["procedures"] +17["api-config.procedure.ts"] +1B["docker-manager.procedure.ts"] +1C["docker-stats.procedure.ts"] +1D["logs.procedure.ts"] +1E["stacks.procedure.ts"] end -subgraph 11["trpc"] -12["index.ts"] -13["router.ts"] -subgraph 14["procedures"] -15["api-config.procedure.ts"] -19["docker-manager.procedure.ts"] -1A["docker-stats.procedure.ts"] -1B["logs.procedure.ts"] -1C["stacks.procedure.ts"] +18["trpc.ts"] end -16["trpc.ts"] end +subgraph G["typings"] +H["database.ts"] +I["docker.ts"] +J["plugin.ts"] +N["elysiajs.ts"] +S["docker-compose.ts"] +W["dockerode.ts"] +1K["websocket.ts"] end -subgraph C["typings"] -D["database.ts"] -E["docker.ts"] -F["elysiajs.ts"] -K["docker-compose.ts"] -Q["dockerode.ts"] -10["plugin.ts"] -1I["websocket.ts"] +subgraph L["middleware"] +M["auth.ts"] end -subgraph G["routes"] -H["stacks.ts"] -1D["api-config.ts"] -1E["docker-manager.ts"] -1F["docker-stats.ts"] -1G["docker-websocket.ts"] -1J["logs.ts"] +subgraph O["routes"] +P["stacks.ts"] +1F["api-config.ts"] +1G["docker-manager.ts"] +1H["docker-stats.ts"] +1I["docker-websocket.ts"] +1L["logs.ts"] end end -7["bun:sqlite"] +5["bun"] +8["events"] B["path"] -subgraph V["fs"] -X["promises"] +E["bun:sqlite"] +subgraph 10["fs"] +12["promises"] end -Z["events"] -18["package.json"] -1H["stream"] -1-->3 +1A["package.json"] +1J["stream"] +1-->4 +1-->M +1-->P 1-->H 1-->D -1-->6 -1-->N 1-->U -1-->12 +1-->Z +1-->14 1-->A -1-->1D -1-->1E 1-->1F 1-->1G -1-->1J -3-->6 -3-->A -3-->D -3-->F -6-->8 -6-->A -6-->D -6-->E -6-->7 -8-->A -A-->6 +1-->1H +1-->1I +1-->1L +4-->7 +4-->D +4-->K +4-->A +4-->I +4-->I +4-->5 +7-->A +7-->I +7-->J +7-->5 +7-->8 +A-->D A-->B -H-->6 -H-->J -H-->A -H-->L -J-->6 -J-->A -J-->D -J-->K -L-->A -L-->F -N-->6 -N-->O -N-->R -N-->A -N-->D -O-->6 -O-->P -O-->A -O-->E -O-->Q +D-->F +D-->A +D-->H +D-->I +D-->E +F-->A +J-->I +K-->A +K-->I +M-->D +M-->A +M-->H +M-->N +P-->D +P-->R P-->A -P-->E -R-->6 -R-->P +P-->T +R-->D +R-->A +R-->H R-->S -U-->W -U-->A -U-->Y +T-->A +T-->N +U-->D U-->V -U-->B -W-->A -W-->X -Y-->A -Y-->E -Y-->10 -Y-->Z -10-->E -12-->13 -13-->15 -13-->19 -13-->1A -13-->1B -13-->1C -13-->16 -15-->16 -15-->6 -15-->A +U-->X +U-->A +U-->H +V-->D +V-->K +V-->A +V-->I +V-->W +X-->D +X-->K +X-->Y +Z-->11 +Z-->A +Z-->7 +Z-->10 +Z-->B +11-->A +11-->12 +14-->15 15-->17 -15-->D +15-->1B +15-->1C +15-->1D +15-->1E +15-->18 17-->18 -19-->16 -19-->6 -19-->A -1A-->16 -1A-->6 -1A-->P -1A-->S -1A-->A -1A-->E -1A-->Q -1B-->16 -1B-->6 +17-->D +17-->A +17-->19 +17-->H +19-->1A +1B-->18 +1B-->D 1B-->A -1C-->16 -1C-->6 -1C-->J +1C-->18 +1C-->D +1C-->K +1C-->Y 1C-->A -1D-->6 -1D-->A -1D-->17 -1D-->L -1D-->3 +1C-->I +1C-->W +1D-->18 1D-->D -1E-->6 +1D-->A +1E-->18 +1E-->D +1E-->R 1E-->A -1E-->L -1F-->6 -1F-->P -1F-->S +1F-->D 1F-->A -1F-->L -1F-->E -1F-->Q -1G-->6 -1G-->P -1G-->S +1F-->19 +1F-->T +1F-->M +1F-->H +1G-->D 1G-->A -1G-->L -1G-->E -1G-->1I -1G-->1H -1I-->1H -1J-->6 -1J-->A +1G-->T +1H-->D +1H-->K +1H-->Y +1H-->A +1H-->T +1H-->I +1H-->W +1I-->D +1I-->K +1I-->Y +1I-->A +1I-->T +1I-->I +1I-->1K +1I-->1J +1K-->1J +1L-->D +1L-->A diff --git a/dependency-graph.svg b/dependency-graph.svg index dcc10989..1537421a 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,1193 +4,1259 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/trpc - -trpc + +trpc cluster_src/core/trpc/procedures - -procedures + +procedures cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings - + +bun + + +bun + + + + + bun:sqlite - - -bun:sqlite + + +bun:sqlite - + events - - -events + + +events - + fs - - -fs + + +fs - + fs/promises - - -promises + + +promises - + package.json - - -package.json + + +package.json - + path - - -path + + +path - + src/core/database/helper.ts - - -helper.ts + + +helper.ts - + src/core/utils/logger.ts - - -logger.ts + + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + - + src/core/database/repository.ts - - -repository.ts + + +repository.ts - + src/core/utils/logger.ts->src/core/database/repository.ts - - - - + + + + src/core/database/repository.ts->bun:sqlite - - + + src/core/database/repository.ts->src/core/database/helper.ts - - - - + + + + src/core/database/repository.ts->src/core/utils/logger.ts - - - - + + + + - + src/typings/database.ts - - -database.ts + + +database.ts src/core/database/repository.ts->src/typings/database.ts - - + + - + src/typings/docker.ts - - -docker.ts + + +docker.ts src/core/database/repository.ts->src/typings/docker.ts - - + + - + src/core/docker/client.ts - - -client.ts + + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->src/typings/docker.ts - - + + + + + +src/core/docker/monitor.ts + + +monitor.ts + + + + + +src/core/docker/monitor.ts->bun + + + + + +src/core/docker/monitor.ts->src/core/utils/logger.ts + + + + + +src/core/docker/monitor.ts->src/core/database/repository.ts + + + + + +src/core/docker/monitor.ts->src/typings/docker.ts + + + + + +src/core/docker/monitor.ts->src/core/docker/client.ts + + + + + +src/core/plugins/plugin-manager.ts + + +plugin-manager.ts + + + + + +src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts + + + + + +src/core/plugins/plugin-manager.ts->bun + + + + + +src/core/plugins/plugin-manager.ts->events + + + + + +src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts + + + + + +src/core/plugins/plugin-manager.ts->src/typings/docker.ts + + + + + +src/typings/plugin.ts + + +plugin.ts + + + + + +src/core/plugins/plugin-manager.ts->src/typings/plugin.ts + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - + src/core/docker/scheduler.ts->src/core/database/repository.ts - - + + - + src/core/docker/scheduler.ts->src/typings/database.ts - - + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - + + - + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + src/typings/dockerode.ts - - -dockerode.ts + + +dockerode.ts - + src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - - -calculations.ts + + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + + + + +src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - - - - -src/core/plugins/plugin-manager.ts - - -plugin-manager.ts - - - - - -src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - - - - -src/core/plugins/plugin-manager.ts->events - - - - - -src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - - - - -src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - - - - -src/typings/plugin.ts - - -plugin.ts - - - - - -src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/repository.ts - - + + - + src/core/stacks/controller.ts->src/typings/database.ts - - + + - + src/typings/docker-compose.ts - - -docker-compose.ts + + +docker-compose.ts - + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + - + src/core/trpc/index.ts - - -index.ts + + +index.ts - + src/core/trpc/router.ts - - -router.ts + + +router.ts - + src/core/trpc/index.ts->src/core/trpc/router.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts - - -api-config.procedure.ts + + +api-config.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - + + - + src/core/trpc/trpc.ts - - -trpc.ts + + +trpc.ts - + src/core/trpc/router.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts - - -docker-manager.procedure.ts + + +docker-manager.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts - - -docker-stats.procedure.ts + + +docker-stats.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts - - -logs.procedure.ts + + +logs.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts - - -stacks.procedure.ts + + +stacks.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - + + - + src/core/utils/package-json.ts->package.json - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/utils/respone-handler.ts - - -respone-handler.ts + + +respone-handler.ts - + src/core/utils/respone-handler.ts->src/core/utils/logger.ts - - + + - + src/typings/elysiajs.ts - - -elysiajs.ts + + +elysiajs.ts - + src/core/utils/respone-handler.ts->src/typings/elysiajs.ts - - + + - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/repository.ts - - + + - + src/index.ts->src/typings/database.ts - - + + + + + +src/index.ts->src/core/docker/monitor.ts + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/core/trpc/index.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/repository.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/repository.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/repository.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->src/core/database/repository.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->src/core/database/repository.ts - - + + - + src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/repository.ts - - + + - + src/routes/docker-websocket.ts->src/typings/docker.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts - - + + - + src/typings/websocket.ts - - -websocket.ts + + +websocket.ts - + src/routes/docker-websocket.ts->src/typings/websocket.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/repository.ts - - + + - + src/typings/websocket.ts->stream - - + + From 16c94da6e4ba81d793b1417355ca752cd6161694 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 18 Mar 2025 22:55:11 +0100 Subject: [PATCH 193/369] Fix: Add routes to get all correctly imported plugins --- src/core/plugins/plugin-manager.ts | 9 ++++++--- src/routes/api-config.ts | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index c030b091..f7b8f95a 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -1,8 +1,7 @@ import { EventEmitter } from "events"; import { logger } from "../utils/logger"; import type { Plugin } from "~/typings/plugin"; -import type { ContainerInfo, HostStats } from "~/typings/docker"; -import { plugin } from "bun"; +import type { ContainerInfo } from "~/typings/docker"; export class PluginManager extends EventEmitter { private plugins: Map = new Map(); @@ -22,6 +21,10 @@ export class PluginManager extends EventEmitter { this.plugins.delete(name); } + getLoadedPlugins(): string[] { + return Array.from(this.plugins.keys()); + } + // Trigger plugin flows: handleContainerStop(containerInfo: ContainerInfo) { this.plugins.forEach((plugin) => { @@ -106,7 +109,7 @@ export class PluginManager extends EventEmitter { plugin.onContainerKill?.(containerInfo); }); } - + handleContainerDie(containerInfo: ContainerInfo) { this.plugins.forEach((plugin) => { plugin.handleContainerDie?.(containerInfo); diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 1c3b13bf..093e390a 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -15,6 +15,7 @@ import { license, } from "~/core/utils/package-json"; import { hashApiKey } from "~/middleware/auth"; +import { pluginManager } from "~/core/plugins/plugin-manager"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) .get( @@ -30,8 +31,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) } catch (error) { return responseHandler.error( set, - "Error getting the DockStatAPI config", error as string, + "Error getting the DockStatAPI config", ); } }, @@ -39,6 +40,21 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], }, ) + .get( + "/plugins", + ({ set }) => { + try { + return pluginManager.getLoadedPlugins(); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error getting all registered plugins", + ); + } + }, + { tags: ["Management"] }, + ) .post( "/update", async ({ set, body }) => { From 06efcc22b726ce3d52b2756559bceefb3236ae05 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 18 Mar 2025 21:56:45 +0000 Subject: [PATCH 194/369] Update dependency graphs --- dependency-graph.dot | 2 +- dependency-graph.mmd | 2 +- dependency-graph.svg | 892 +++++++++++++++++++++---------------------- 3 files changed, 448 insertions(+), 448 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 06b5870f..8759b82b 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -55,7 +55,6 @@ strict digraph "dependency-cruiser output"{ "src/core/plugins/plugin-manager.ts" -> "src/core/utils/logger.ts" "src/core/plugins/plugin-manager.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] "src/core/plugins/plugin-manager.ts" -> "src/typings/plugin.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/plugins/plugin-manager.ts" -> "bun" "src/core/plugins/plugin-manager.ts" -> "events" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/stacks" {label="stacks" "src/core/stacks/controller.ts" [label= tooltip="controller.ts" URL="src/core/stacks/controller.ts" fillcolor="#ddfeff"] } } } "src/core/stacks/controller.ts" -> "src/core/database/repository.ts" @@ -133,6 +132,7 @@ strict digraph "dependency-cruiser output"{ "src/middleware/auth.ts" -> "src/typings/elysiajs.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/api-config.ts" [label= tooltip="api-config.ts" URL="src/routes/api-config.ts" fillcolor="#ddfeff"] } } "src/routes/api-config.ts" -> "src/core/database/repository.ts" + "src/routes/api-config.ts" -> "src/core/plugins/plugin-manager.ts" "src/routes/api-config.ts" -> "src/core/utils/logger.ts" "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" "src/routes/api-config.ts" -> "src/core/utils/respone-handler.ts" diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 36b5db96..0e9f5167 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -101,7 +101,6 @@ end 7-->A 7-->I 7-->J -7-->5 7-->8 A-->D A-->B @@ -179,6 +178,7 @@ Z-->B 1E-->R 1E-->A 1F-->D +1F-->7 1F-->A 1F-->19 1F-->T diff --git a/dependency-graph.svg b/dependency-graph.svg index 1537421a..7ea0c591 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,82 +4,82 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/trpc - -trpc + +trpc cluster_src/core/trpc/procedures - -procedures + +procedures cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -87,8 +87,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -96,8 +96,8 @@ events - -events + +events @@ -105,8 +105,8 @@ fs - -fs + +fs @@ -114,8 +114,8 @@ fs/promises - -promises + +promises @@ -123,8 +123,8 @@ package.json - -package.json + +package.json @@ -132,8 +132,8 @@ path - -path + +path @@ -141,8 +141,8 @@ src/core/database/helper.ts - -helper.ts + +helper.ts @@ -150,445 +150,439 @@ src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + src/core/database/repository.ts - -repository.ts + +repository.ts - + src/core/utils/logger.ts->src/core/database/repository.ts - - - - + + + + src/core/database/repository.ts->bun:sqlite - - + + src/core/database/repository.ts->src/core/database/helper.ts - - - - + + + + src/core/database/repository.ts->src/core/utils/logger.ts - - - - + + + + src/typings/database.ts - -database.ts + +database.ts src/core/database/repository.ts->src/typings/database.ts - - + + src/typings/docker.ts - -docker.ts + +docker.ts src/core/database/repository.ts->src/typings/docker.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->bun - - + + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->src/core/database/repository.ts - - + + src/core/docker/monitor.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - - - - -src/core/plugins/plugin-manager.ts->bun - - + + - + src/core/plugins/plugin-manager.ts->events - - + + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + src/typings/plugin.ts - -plugin.ts + +plugin.ts src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/repository.ts - - + + src/core/docker/scheduler.ts->src/typings/database.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + src/typings/dockerode.ts - -dockerode.ts + +dockerode.ts src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/repository.ts - - + + - + src/core/stacks/controller.ts->src/typings/database.ts - - + + src/typings/docker-compose.ts - -docker-compose.ts + +docker-compose.ts - + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + src/core/trpc/index.ts - -index.ts + +index.ts @@ -596,634 +590,640 @@ src/core/trpc/router.ts - -router.ts + +router.ts - + src/core/trpc/index.ts->src/core/trpc/router.ts - - + + src/core/trpc/procedures/api-config.procedure.ts - -api-config.procedure.ts + +api-config.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - + + src/core/trpc/trpc.ts - -trpc.ts + +trpc.ts - + src/core/trpc/router.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts - -docker-manager.procedure.ts + +docker-manager.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts - -docker-stats.procedure.ts + +docker-stats.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - + + src/core/trpc/procedures/logs.procedure.ts - -logs.procedure.ts + +logs.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - + + src/core/trpc/procedures/stacks.procedure.ts - -stacks.procedure.ts + +stacks.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - + + - + src/core/utils/package-json.ts->package.json - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/utils/respone-handler.ts - -respone-handler.ts + +respone-handler.ts - + src/core/utils/respone-handler.ts->src/core/utils/logger.ts - - + + src/typings/elysiajs.ts - -elysiajs.ts + +elysiajs.ts - + src/core/utils/respone-handler.ts->src/typings/elysiajs.ts - - + + src/index.ts - -index.ts + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/repository.ts - - + + - + src/index.ts->src/typings/database.ts - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/core/trpc/index.ts - - + + src/middleware/auth.ts - -auth.ts + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + src/routes/stacks.ts - -stacks.ts + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + src/routes/api-config.ts - -api-config.ts + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + src/routes/docker-manager.ts - -docker-manager.ts + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + src/routes/docker-stats.ts - -docker-stats.ts + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + src/routes/docker-websocket.ts - -docker-websocket.ts + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + src/routes/logs.ts - -logs.ts + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/repository.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + src/routes/stacks.ts->src/core/utils/logger.ts - - + + src/routes/stacks.ts->src/core/database/repository.ts - - + + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + src/routes/stacks.ts->src/core/utils/respone-handler.ts - - + + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/repository.ts - - + + src/routes/api-config.ts->src/typings/database.ts - - + + + + + +src/routes/api-config.ts->src/core/plugins/plugin-manager.ts + + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + src/routes/api-config.ts->src/core/utils/respone-handler.ts - - + + src/routes/api-config.ts->src/middleware/auth.ts - - + + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + src/routes/docker-manager.ts->src/core/database/repository.ts - - + + src/routes/docker-manager.ts->src/core/utils/respone-handler.ts - - + + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + src/routes/docker-stats.ts->src/core/database/repository.ts - - + + src/routes/docker-stats.ts->src/typings/docker.ts - - + + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + src/routes/docker-stats.ts->src/core/utils/respone-handler.ts - - + + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + src/routes/docker-websocket.ts->src/core/database/repository.ts - - + + src/routes/docker-websocket.ts->src/typings/docker.ts - - + + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts - - + + src/typings/websocket.ts - -websocket.ts + +websocket.ts src/routes/docker-websocket.ts->src/typings/websocket.ts - - + + @@ -1237,26 +1237,26 @@ src/routes/docker-websocket.ts->stream - - + + src/routes/logs.ts->src/core/utils/logger.ts - - + + src/routes/logs.ts->src/core/database/repository.ts - - + + src/typings/websocket.ts->stream - - + + From a23282c73297b835d64f44623198f84cd56735d3 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 09:49:43 +0100 Subject: [PATCH 195/369] Fix: Sourcery suggesions, knip adjustments, stack controller refactor, swagger descriptions --- .knip.json | 5 +- bun.lock | 161 ++--------- package.json | 7 +- src/core/database/repository.ts | 45 ++-- src/core/docker/monitor.ts | 4 +- src/core/plugins/plugin-manager.ts | 2 +- src/core/stacks/controller.ts | 267 ++++++++++++------- src/core/trpc/procedures/stacks.procedure.ts | 17 +- src/core/trpc/router.ts | 2 - src/core/trpc/trpc.ts | 2 +- src/core/utils/respone-handler.ts | 6 +- src/routes/api-config.ts | 17 +- src/routes/docker-manager.ts | 3 + src/routes/docker-stats.ts | 3 + src/routes/logs.ts | 5 + src/routes/stacks.ts | 91 ++++--- src/typings/database.ts | 10 +- 17 files changed, 316 insertions(+), 331 deletions(-) diff --git a/.knip.json b/.knip.json index 14f7f539..64b44a6d 100644 --- a/.knip.json +++ b/.knip.json @@ -1,4 +1,5 @@ { "entry": ["src/index.ts"], - "project": ["src/**/*.ts"] -} \ No newline at end of file + "project": ["src/**/*.ts"], + "ignore": ["src/plugins/*.plugin.ts"] +} diff --git a/bun.lock b/bun.lock index c9f06ca7..9a3a82fc 100644 --- a/bun.lock +++ b/bun.lock @@ -8,12 +8,12 @@ "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "@elysiajs/trpc": "^1.1.0", - "@elysiajs/websocket": "^0.2.8", "@trpc/server": "^10.45.2", "chalk": "^5.4.1", "docker-compose": "^1.1.1", "dockerode": "^4.0.4", "elysia": "latest", + "knip": "latest", "split2": "^4.2.0", "winston": "^3.17.0", "yaml": "^2.7.0", @@ -24,10 +24,9 @@ "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", - "dependency-cruiser": "^16.10.0", - "knip": "^5.46.0", "typescript": "^5.8.2", "wrap-ansi": "^9.0.0", + "zod": "^3.24.2", }, }, }, @@ -49,9 +48,7 @@ "@elysiajs/trpc": ["@elysiajs/trpc@1.1.0", "", { "peerDependencies": { "elysia": ">= 1.1.0" } }, "sha512-M8QWC+Wa5Z5MWY/+uMQuwZ+JoQkp4jOc1ra4SncFy1zSjFGin59LO1AT0pE+DRJaFV17gha9y7cB6Q7GnaJEAw=="], - "@elysiajs/websocket": ["@elysiajs/websocket@0.2.8", "", { "dependencies": { "nanoid": "^4.0.0", "raikiri": "^0.0.0-beta.3" }, "peerDependencies": { "elysia": ">= 0.2.2" } }, "sha512-K9KLmYL1SYuAV353GvmK0V9DG5w7XTOGsa1H1dGB5BUTzvBaMvnwNeqnJQ3cjf9V1c0EjQds0Ty4LfUFvV45jw=="], - - "@grpc/grpc-js": ["@grpc/grpc-js@1.12.6", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.13.0", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-pMuxInZjUnUkgMT2QLZclRqwk2ykJbIU05aZgPgJYXEpN9+2I7z7aNwcjWZSycRPl232FfhPszyBFJyOxTHNog=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], @@ -85,11 +82,11 @@ "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], - "@scalar/themes": ["@scalar/themes@0.9.68", "", { "dependencies": { "@scalar/types": "0.0.34" } }, "sha512-466ac2fdQJOBBSLkGUf88vuZVF+qNMeVpjb0aAHrKkxhpjucTPKdTYO8r2dsX1R5k9A13gWPnm594VW5G/bGHw=="], + "@scalar/themes": ["@scalar/themes@0.9.80", "", { "dependencies": { "@scalar/types": "0.1.2" } }, "sha512-UZM8pQLpGeBtOdUx6yOcj5SPiWo1LaylUVt8HjCRFQ90zZtwbcIWfUWwWOay5nh7cwSVqY2G9eAyGYcNJB12ew=="], "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], - "@sinclair/typebox": ["@sinclair/typebox@0.34.27", "", {}, "sha512-C7mxE1VC3WC2McOufZXEU48IfRVI+BcKxk4NOyNn3+JMUNdJHEWGS5CqjuDX+ij2NCCz8/nse1mT7yn8Fv2GHg=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.30", "", {}, "sha512-gFB3BiqjDxEoadW0zn+xyMVb7cLxPCoblVn2C/BKpI41WPYi2d6fwHAlynPNZ5O/Q4WEiujdnJzVtvG/Jc2CBQ=="], "@snyk/github-codeowners": ["@snyk/github-codeowners@1.1.0", "", { "dependencies": { "commander": "^4.1.1", "ignore": "^5.1.8", "p-map": "^4.0.0" }, "bin": { "github-codeowners": "dist/cli.js" } }, "sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw=="], @@ -97,7 +94,7 @@ "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], - "@types/dockerode": ["@types/dockerode@3.3.34", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-mH9SuIb8NuTDsMus5epcbTzSbEo52fKLBMo0zapzYIAIyfDqoIFn7L3trekHLKC8qmxGV++pPUP4YqQ9n5v2Zg=="], + "@types/dockerode": ["@types/dockerode@3.3.35", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-P+DCMASlsH+QaKkDpekKrP5pLls767PPs+/LrlVbKnEnY5tMpEUa2C6U4gRsdFZengOqxdCIqy16R22Q3pLB6Q=="], "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], @@ -109,23 +106,11 @@ "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], - "@unhead/schema": ["@unhead/schema@1.11.19", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg=="], - - "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "acorn-jsx-walk": ["acorn-jsx-walk@2.0.0", "", {}, "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA=="], - - "acorn-loose": ["acorn-loose@8.4.0", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ=="], - - "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -147,7 +132,7 @@ "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], - "bun-types": ["bun-types@1.2.3", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P7AeyTseLKAvgaZqQrvp3RqFM3yN9PlcLuSTe7SoJOfZkER73mLdT2vEQi8U64S1YvM/ldcNiQjn0Sn7H9lGgg=="], + "bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="], "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], @@ -161,15 +146,15 @@ "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], - "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], @@ -183,9 +168,7 @@ "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - "dependency-cruiser": ["dependency-cruiser@16.10.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "acorn-jsx-walk": "^2.0.0", "acorn-loose": "^8.4.0", "acorn-walk": "^8.3.4", "ajv": "^8.17.1", "commander": "^13.1.0", "enhanced-resolve": "^5.18.1", "ignore": "^7.0.3", "interpret": "^3.1.1", "is-installed-globally": "^1.0.0", "json5": "^2.2.3", "memoize": "^10.0.0", "picocolors": "^1.1.1", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rechoir": "^0.8.0", "safe-regex": "^2.1.1", "semver": "^7.7.1", "teamcity-service-messages": "^0.1.14", "tsconfig-paths-webpack-plugin": "^4.2.0", "watskeburt": "^4.2.3" }, "bin": { "dependency-cruiser": "bin/dependency-cruise.mjs", "dependency-cruise": "bin/dependency-cruise.mjs", "depcruise": "bin/dependency-cruise.mjs", "depcruise-baseline": "bin/depcruise-baseline.mjs", "depcruise-fmt": "bin/depcruise-fmt.mjs", "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs" } }, "sha512-o6pEB8X/XS0AjpQBhPJW3pSY7HIviRM7+G601T9ruV63NVJC4DxLMA+a1VzZlKOzO2fO6JKRHjRmGjzZZHEFYA=="], - - "docker-compose": ["docker-compose@1.1.1", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-UkIUz0LtzuO17Ijm6SXMGtfZMs7IvbNwvuJBiBuN93PIhr/n9/sbJMqpvYFaCBGfwu1ZM4PPPDgQzeeke4lEoA=="], + "docker-compose": ["docker-compose@1.2.0", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-wIU1eHk3Op7dFgELRdmOYlPYS4gP8HhH1ZmZa13QZF59y0fblzFDFmKPhyc05phCy2hze9OEvNZAsoljrs+72w=="], "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], @@ -193,7 +176,7 @@ "easy-table": ["easy-table@1.2.0", "", { "dependencies": { "ansi-regex": "^5.0.1" }, "optionalDependencies": { "wcwidth": "^1.0.1" } }, "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww=="], - "elysia": ["elysia@1.2.21", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-E9b1JcB7fiQ2ptk24W8OnBrMYUoKzffIXob9uTVUKhqOKxaXAd9UyWBeyr7JCDa/VD/b/9S8aIey9/YJsK5sLg=="], + "elysia": ["elysia@1.2.25", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-WsdQpORJvb4uszzeqYT0lg97knw1iBW1NTzJ1Jm57tiHg+DfAotlWXYbjmvQ039ssV0fYELDHinLLoUazZkEHg=="], "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], @@ -205,12 +188,8 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], @@ -221,52 +200,34 @@ "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="], - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "ignore": ["ignore@7.0.3", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], - - "interpret": ["interpret@3.1.1", "", {}, "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ=="], - "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-installed-globally": ["is-installed-globally@1.0.0", "", { "dependencies": { "global-directory": "^4.0.1", "is-path-inside": "^4.0.0" } }, "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ=="], - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], - "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -275,12 +236,6 @@ "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - "knip": ["knip@5.46.0", "", { "dependencies": { "@nodelib/fs.walk": "3.0.1", "@snyk/github-codeowners": "1.1.0", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.0", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "summary": "2.1.0", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-WedHSK5xNBWYgm64Rt5B9b0CVXL2kRBcyCeet3NHgdv9en3QE4AWSDPEiX48NoPUBW3h//9S0VwLF5MG/MPi3g=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], @@ -293,23 +248,17 @@ "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], - "memoize": ["memoize@10.1.0", "", { "dependencies": { "mimic-function": "^5.0.1" } }, "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nan": ["nan@2.22.1", "", {}, "sha512-pfRR4ZcNTSm2ZFHaztuvbICf+hyiG6ecA06SfAxoPmuHjvMu0KUIae7Y8GyVkbBqeEIidsmXeYooWIX9+qjfRQ=="], - - "nanoid": ["nanoid@4.0.2", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw=="], + "nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="], "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], @@ -325,8 +274,6 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -335,50 +282,32 @@ "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], - "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "raikiri": ["raikiri@0.0.0-beta.8", "", {}, "sha512-cH/yfvkiGkN8IBB2MkRHikpPurTnd2sMkQ/xtGpXrp3O76P4ppcWPb+86mJaBDzKaclLnSX+9NnT79D7ifH4/w=="], - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "rechoir": ["rechoir@0.8.0", "", { "dependencies": { "resolve": "^1.20.0" } }, "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ=="], - - "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - - "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "safe-regex": ["safe-regex@2.1.1", "", { "dependencies": { "regexp-tree": "~0.1.1" } }, "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A=="], - "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="], "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], @@ -395,34 +324,22 @@ "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="], "summary": ["summary@2.1.0", "", {}, "sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], "tar-fs": ["tar-fs@2.0.1", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.0.0" } }, "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "teamcity-service-messages": ["teamcity-service-messages@0.1.14", "", {}, "sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w=="], - "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], - "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], - - "tsconfig-paths-webpack-plugin": ["tsconfig-paths-webpack-plugin@4.2.0", "", { "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", "tapable": "^2.2.1", "tsconfig-paths": "^4.1.2" } }, "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA=="], - "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], @@ -433,8 +350,6 @@ "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - "watskeburt": ["watskeburt@4.2.3", "", { "bin": { "watskeburt": "dist/run-cli.js" } }, "sha512-uG9qtQYoHqAsnT711nG5iZc/8M5inSmkGCOp7pFaytKG2aTfIca7p//CjiVzAE4P7hzaYuCozMjNNaLgmhbK5g=="], - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -463,23 +378,9 @@ "@nodelib/fs.scandir/@nodelib/fs.stat": ["@nodelib/fs.stat@4.0.0", "", {}, "sha512-ctr6bByzksKRCV0bavi8WoQevU6plSp2IkllIsEqaiKe2mwNNnaluhnRhcsgGZHrrHk57B3lf95MkLMO3STYcg=="], - "@scalar/themes/@scalar/types": ["@scalar/types@0.0.34", "", { "dependencies": { "@scalar/openapi-types": "0.1.8", "@unhead/schema": "^1.11.11" } }, "sha512-q01ctijmHArM5KOny2zU+sHfhpsgOAENrDENecK2TsQNn5FYLmFZouMKeW2M6F7KFLPZnFxUiL/rT88b6Rp/Kg=="], - - "@snyk/github-codeowners/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - - "@snyk/github-codeowners/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "@types/docker-modem/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], - - "@types/dockerode/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "@scalar/themes/@scalar/types": ["@scalar/types@0.1.2", "", { "dependencies": { "@scalar/openapi-types": "0.1.9", "@unhead/schema": "^1.11.11", "zod": "^3.23.8" } }, "sha512-5kCLQRwAYWt1ds110EaUb9yonc3KoQYNyo4YUCigJLOnoNugbqkEX0zRudGevItiuk+xg4uOYd30r3C+6xAasA=="], - "@types/split2/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], - - "@types/ssh2/@types/node": ["@types/node@18.19.76", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw=="], - - "@types/ws/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], - - "bun-types/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + "@types/ssh2/@types/node": ["@types/node@18.19.80", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ=="], "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -487,40 +388,36 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "color-string/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + "easy-table/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "fast-glob/@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "protobufjs/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], - - "strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - - "tsconfig-paths-webpack-plugin/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.8", "", {}, "sha512-iufA5/6hPCmRIVD2eh7qGpoKvoA08Gw/qUb2JECifBtAwA93fo7+1k9uHK440f2LMJsbxIzA+nv7RS0BmfiO/g=="], + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="], "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "fast-glob/@nodelib/fs.walk/@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - "tsconfig-paths-webpack-plugin/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], } } diff --git a/package.json b/package.json index da501770..3426d545 100644 --- a/package.json +++ b/package.json @@ -25,12 +25,12 @@ "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "@elysiajs/trpc": "^1.1.0", - "@elysiajs/websocket": "^0.2.8", "@trpc/server": "^10.45.2", "chalk": "^5.4.1", "docker-compose": "^1.1.1", "dockerode": "^4.0.4", "elysia": "latest", + "knip": "latest", "split2": "^4.2.0", "winston": "^3.17.0", "yaml": "^2.7.0" @@ -41,10 +41,9 @@ "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", - "dependency-cruiser": "^16.10.0", - "knip": "^5.46.0", "typescript": "^5.8.2", - "wrap-ansi": "^9.0.0" + "wrap-ansi": "^9.0.0", + "zod": "^3.24.2" }, "module": "src/index.js", "trustedDependencies": [ diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index d5f11d60..552b6a2b 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -155,8 +155,7 @@ export const dbFunctions = { FROM docker_hosts ORDER BY name DESC `); - const data = stmt.all(); - return data as DockerHost[]; + return stmt.all() as DockerHost[]; }, () => {}, ); @@ -194,8 +193,7 @@ export const dbFunctions = { FROM backend_log_entries ORDER BY timestamp DESC `); - const data = stmt.all(); - return data; + return stmt.all(); }, () => {}, ); @@ -211,8 +209,7 @@ export const dbFunctions = { WHERE level = ? ORDER BY timestamp DESC `); - const data = stmt.all(level); - return data; + return stmt.all(level); }, () => { if (typeof level !== "string") { @@ -232,8 +229,7 @@ export const dbFunctions = { SET url = ?, secure = ? WHERE name = ? `); - const data = stmt.run(url, secure, name); - return data; + return stmt.run(url, secure, name); }, () => { if ( @@ -256,8 +252,7 @@ export const dbFunctions = { DELETE FROM docker_hosts WHERE name = ? `); - const data = stmt.run(name); - return data; + return stmt.run(name); }, () => { if (typeof name !== "string") { @@ -275,8 +270,7 @@ export const dbFunctions = { const stmt = db.prepare(` DELETE FROM backend_log_entries `); - const data = stmt.run(); - return data; + return stmt.run(); }, () => {}, ); @@ -290,8 +284,7 @@ export const dbFunctions = { DELETE FROM backend_log_entries WHERE level = ? `); - const data = stmt.run(level); - return data; + return stmt.run(level); }, () => { if (typeof level !== "string") { @@ -316,8 +309,7 @@ export const dbFunctions = { keep_data_for = ?, api_key = ? `); - const data = stmt.run(fetching_interval, keep_data_for, api_key); - return data; + return stmt.run(fetching_interval, keep_data_for, api_key); }, () => { if ( @@ -339,8 +331,7 @@ export const dbFunctions = { SELECT keep_data_for, fetching_interval, api_key FROM config `); - const data = stmt.all(); - return data; + return stmt.all(); }, () => {}, ); @@ -388,7 +379,7 @@ export const dbFunctions = { INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); - const data = stmt.run( + return stmt.run( id, hostId, name, @@ -398,7 +389,6 @@ export const dbFunctions = { cpu_usage, memory_usage, ); - return data; }, () => { if ( @@ -454,7 +444,7 @@ export const dbFunctions = { containersPaused = excluded.containersPaused, images = excluded.images; `); - const data = stmt.run( + return stmt.run( stats.hostId, stats.dockerVersion, stats.apiVersion, @@ -469,7 +459,6 @@ export const dbFunctions = { stats.containersPaused, stats.images, ); - return data; }, () => {}, ); @@ -492,7 +481,7 @@ export const dbFunctions = { ) VALUES(?, ?, ?, ?, ?, ?, ?, ?) `); - const data = stmt.run( + return stmt.run( stack_config.name, stack_config.version, stack_config.custom, @@ -502,7 +491,6 @@ export const dbFunctions = { stack_config.automatic_reboot_on_error, stack_config.image_updates, ); - return data; }, () => {}, ); @@ -517,8 +505,7 @@ export const dbFunctions = { FROM stacks_config ORDER BY name DESC `); - const data = stmt.all(); - return data; + return stmt.all(); }, () => {}, ); @@ -532,8 +519,7 @@ export const dbFunctions = { DELETE FROM stacks_config WHERE name = ?; `); - const data = stmt.run(name); - return data; + return stmt.run(name); }, () => {}, ); @@ -555,7 +541,7 @@ export const dbFunctions = { image_updates = ? WHERE name = ?; `); - const data = stmt.run( + return stmt.run( stack_config.version, stack_config.custom, stack_config.source, @@ -565,7 +551,6 @@ export const dbFunctions = { stack_config.image_updates, stack_config.name, ); - return data; }, () => {}, ); diff --git a/src/core/docker/monitor.ts b/src/core/docker/monitor.ts index fe8df259..4f56b2b0 100644 --- a/src/core/docker/monitor.ts +++ b/src/core/docker/monitor.ts @@ -49,7 +49,9 @@ async function startFor(host: DockerHost) { buffer = lines.pop() || ""; for (const line of lines) { - if (line.trim() === "") continue; + if (line.trim() === "") { + continue; + } let event: any; try { diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index f7b8f95a..55453d0a 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -3,7 +3,7 @@ import { logger } from "../utils/logger"; import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo } from "~/typings/docker"; -export class PluginManager extends EventEmitter { +class PluginManager extends EventEmitter { private plugins: Map = new Map(); register(plugin: Plugin) { diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 2ff5edb9..f4af21a9 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -5,130 +5,195 @@ import DockerCompose from "docker-compose"; import type { Stack, ComposeSpec } from "~/typings/docker-compose"; import type { stacks_config } from "~/typings/database"; +async function runStackCommand( + stack_name: string, + command: (cwd: string) => Promise, + action: string, +): Promise { + try { + const stack = { name: stack_name }; + const stackPath = await getStackPath(stack as Stack); + return await command(stackPath); + } catch (error: any) { + throw new Error( + `Error while ${action} stack "${stack_name}": ${error.message || error}`, + ); + } +} + async function getStackPath(stack: Stack): Promise { - const stackName = stack.name.trim().replace(/\s+/g, "_"); - return `stacks/${stackName}`; + const stackName = stack.name.trim().replace(/\s+/g, "_"); + return `stacks/${stackName}`; } async function createStackYAML(compose_spec: Stack): Promise { - const yaml = YAML.stringify(compose_spec.compose_spec); - const stackPath = await getStackPath(compose_spec); - await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { createPath: true }); + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); } export async function deployStack( - stack: ComposeSpec, - name: string, - version: number, - source: string, - automatic_reboot_on_error: boolean, - isCustom: boolean, - image_updates: boolean, - stack_prefix?: string + stack: ComposeSpec, + name: string, + version: number, + source: string, + automatic_reboot_on_error: boolean, + isCustom: boolean, + image_updates: boolean, + stack_prefix?: string, ): Promise { - try { - logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`) - - const serviceCount = stack.services - ? Object.keys(stack.services).length - : 0; - - const resolvedPrefix = stack_prefix ?? ""; - - const stack_config: stacks_config = { - name: name, - version: version, - source, - stack_prefix: resolvedPrefix, - automatic_reboot_on_error, - container_count: serviceCount, - custom: isCustom, - image_updates, - }; - - if (!stack.name) { - logger.debug(`${JSON.stringify(stack)}`) - throw new Error("Stack name needed") - } - - dbFunctions.addStack(stack_config); - - const stackYaml: Stack = { - name: name, - source: source, - version: version, - compose_spec: stack, - } - await createStackYAML(stackYaml); - const stackPath = await getStackPath(stackYaml); - await DockerCompose.upAll({ cwd: stackPath }); - } catch (error: any) { - throw new Error(`Error while deploying Stack: ${error.message || error}`); + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`); + + const serviceCount = stack.services + ? Object.keys(stack.services).length + : 0; + + const resolvedPrefix = stack_prefix ?? ""; + + const stack_config: stacks_config = { + name: name, + version: version, + source, + stack_prefix: resolvedPrefix, + automatic_reboot_on_error, + container_count: serviceCount, + custom: isCustom, + image_updates, + }; + + if (!stack.name) { + logger.debug(`${JSON.stringify(stack)}`); + throw new Error("Stack name needed"); } + + dbFunctions.addStack(stack_config); + + const stackYaml: Stack = { + name: name, + source: source, + version: version, + compose_spec: stack, + }; + await createStackYAML(stackYaml); + const stackPath = await getStackPath(stackYaml); + await DockerCompose.upAll({ cwd: stackPath }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } export async function stopStack(stack_name: string): Promise { - try { - const stack = { - name: stack_name - } - const stackPath = await getStackPath(stack as Stack); - await DockerCompose.downAll({ cwd: stackPath }); - } catch (error: any) { - throw new Error(`Error while stopping stack "${stack_name}": ${error.message || error}`); - } + try { + await runStackCommand( + stack_name, + (cwd) => DockerCompose.downAll({ cwd }), + "stopping", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } export async function startStack(stack_name: string): Promise { - try { - const stack = { - name: stack_name - } - const stackPath = await getStackPath(stack as Stack); - await DockerCompose.upAll({ cwd: stackPath }); - } catch (error: any) { - throw new Error(`Error while starting stack "${stack_name}": ${error.message || error}`); - } + try { + await runStackCommand( + stack_name, + (cwd) => DockerCompose.upAll({ cwd }), + "starting", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } export async function pullStackImages(stack_name: string): Promise { - try { - const stack = { - name: stack_name - } - const stackPath = await getStackPath(stack as Stack); - await DockerCompose.pullAll({ cwd: stackPath }); - } catch (error: any) { - throw new Error(`Error while pulling images for stack "${stack_name}": ${error.message || error}`); - } + try { + await runStackCommand( + stack_name, + (cwd) => DockerCompose.pullAll({ cwd }), + "pulling images for", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } export async function restartStack(stack_name: string): Promise { - try { - const stack = { - name: stack_name - } - const stackPath = await getStackPath(stack as Stack); - await DockerCompose.restartAll({ cwd: stackPath }); - } catch (error: any) { - throw new Error(`Error while restarting stack "${stack_name}": ${error.message || error}`); - } + try { + await runStackCommand( + stack_name, + (cwd) => DockerCompose.restartAll({ cwd }), + "restarting", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } -export async function getStackStatus(stack_name: string): Promise { - try { - logger.debug("Retrieving status for Stack:", stack_name); - const stackYaml = { name: stack_name }; - const stackPath = await getStackPath(stackYaml as Stack); - const rawStatus = await DockerCompose.ps({ cwd: stackPath }); - +export async function getStackStatus(stack_name: string): Promise { + try { + return await runStackCommand( + stack_name, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[(service.name)] = service.state; - return acc; - }, {}); - - } catch (error: any) { - throw new Error(`Error while retrieving status for stack "${stack_name}": ${error.message || error}`); - } + acc[service.name] = service.state; + return acc; + }, {}); + }, + "retrieving status for", + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } +export async function getAllStacksStatus(): Promise> { + try { + const stacks = dbFunctions.getStacks() as stacks_config[]; + + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.name, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "retrieving status for", + ); + return { stackName: stack.name, status }; + }), + ); + + return statusResults.reduce( + (acc, { stackName, status }) => { + acc[stackName] = status; + return acc; + }, + {} as Record, + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} diff --git a/src/core/trpc/procedures/stacks.procedure.ts b/src/core/trpc/procedures/stacks.procedure.ts index 6aad4e38..db7a85c1 100644 --- a/src/core/trpc/procedures/stacks.procedure.ts +++ b/src/core/trpc/procedures/stacks.procedure.ts @@ -37,11 +37,18 @@ export const stacksProcedure = router({ .mutation(async ({ input }) => { try { const missingParams = []; - if (!input.compose_spec) missingParams.push("compose_spec"); - if (!input.automatic_reboot_on_error) + if (!input.compose_spec) { + missingParams.push("compose_spec"); + } + if (!input.automatic_reboot_on_error) { missingParams.push("automatic_reboot_on_error"); - if (!input.source) missingParams.push("source"); - if (!input.name) missingParams.push("name"); + } + if (!input.source) { + missingParams.push("source"); + } + if (!input.name) { + missingParams.push("name"); + } if (missingParams.length > 0) { throw new TRPCError({ @@ -58,7 +65,7 @@ export const stacksProcedure = router({ input.automatic_reboot_on_error, input.isCustom || false, input.image_updates || false, - input.stack_prefix + input.stack_prefix, ); logger.info(`Deployed Stack (${input.name}) via tRPC`); diff --git a/src/core/trpc/router.ts b/src/core/trpc/router.ts index acdd78d0..9e0bddfc 100644 --- a/src/core/trpc/router.ts +++ b/src/core/trpc/router.ts @@ -17,5 +17,3 @@ export const appRouter = router({ check: t.procedure.query(() => ({ status: "healthy" })), }), }); - -export type AppRouter = typeof appRouter; diff --git a/src/core/trpc/trpc.ts b/src/core/trpc/trpc.ts index 554f58dd..c7813f91 100644 --- a/src/core/trpc/trpc.ts +++ b/src/core/trpc/trpc.ts @@ -1,5 +1,5 @@ import { initTRPC } from "@trpc/server"; export const t = initTRPC.create(); -export const router = t.router; +export const { router } = t; export const publicProcedure = t.procedure; diff --git a/src/core/utils/respone-handler.ts b/src/core/utils/respone-handler.ts index 65b7c09f..369e9171 100644 --- a/src/core/utils/respone-handler.ts +++ b/src/core/utils/respone-handler.ts @@ -19,10 +19,10 @@ export const responseHandler = { return { success: true }; }, - simple_error(set: set, response_massage: string, status_code?: number) { + simple_error(set: set, response_message: string, status_code?: number) { set.status = status_code || 502; - logger.warn(response_massage); - return { error: response_massage }; + logger.warn(response_message); + return { error: response_message }; }, reject(set: set, reject: any, response_message: string, error?: string) { diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 093e390a..a74f34b7 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -37,7 +37,10 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) } }, { - tags: ["Management"], + detail: { + tags: ["Management"], + description: "Returns DockStatAPI's config", + }, }, ) .get( @@ -53,7 +56,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) ); } }, - { tags: ["Management"] }, + { detail: { tags: ["Management"], description: "List all Plugin Names" } }, ) .post( "/update", @@ -81,7 +84,10 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) keep_data_for: t.Number(), api_key: t.String(), }), - tags: ["Management"], + detail: { + tags: ["Management"], + description: "Update the current DockStatAPI config", + }, }, ) .get( @@ -109,6 +115,9 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) } }, { - tags: ["Management"], + detail: { + tags: ["Management"], + description: "Returns relevant information about the package.json", + }, }, ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index eb53fdb9..1d351013 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -23,6 +23,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) { detail: { tags: ["Management"], + description: "Add a new Host as Monitoring target", }, body: t.Object({ name: t.String(), @@ -49,6 +50,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) { detail: { tags: ["Management"], + description: "Update an already existing target's config", }, body: t.Object({ name: t.String(), @@ -77,6 +79,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) { detail: { tags: ["Management"], + description: "Returns an Array of Host-config-objects", }, }, ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index d85bfc1f..9eb34036 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -101,6 +101,8 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) { detail: { tags: ["Statistics"], + description: + "Fetches all Containers and their statistics across all Hosts", }, }, ) @@ -152,6 +154,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) { detail: { tags: ["Statistics"], + description: "Fetches the Host Stats for a specified Host", }, }, ); diff --git a/src/routes/logs.ts b/src/routes/logs.ts index a8cae1c5..f5cf3cbe 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -20,6 +20,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], + description: "Retrieves all Logs which have been saved in the Database", }, }, ) @@ -41,6 +42,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], + description: "Retrieves all Logs with the specified level", }, }, ) @@ -62,6 +64,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], + description: "Deletes all Logs which are saved in the Database", }, }, ) @@ -83,6 +86,8 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], + description: + "Deletes all Logs with the specified Level inside the Database", }, }, ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index f4be7b57..c4e6c2cb 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -7,6 +7,7 @@ import { restartStack, getStackStatus, startStack, + getAllStacksStatus, } from "~/core/stacks/controller"; import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; @@ -47,23 +48,27 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body.automatic_reboot_on_error, isCustom, image_updates, - body.stack_prefix + body.stack_prefix, ); logger.info(`Deployed Stack (${body.name})`); return responseHandler.ok( set, - `Stack ${body.name} deployed successfully` + `Stack ${body.name} deployed successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error deploying stack" + "Error deploying stack", ); } }, { - detail: { tags: ["Stacks"] }, + detail: { + tags: ["Stacks"], + description: + "Deploy a Stack, either with a prebuilt one or provide your own structure", + }, body: t.Object({ compose_spec: t.Any(), name: t.String(), @@ -74,7 +79,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) source: t.String(), stack_prefix: t.Optional(t.String()), }), - } + }, ) .post( "/start", @@ -87,22 +92,22 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Started Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} started successfully` + `Stack ${body.stack} started successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error starting stack" + "Error starting stack", ); } }, { - detail: { tags: ["Stacks"] }, + detail: { tags: ["Stacks"], description: "Start a specific Stack" }, body: t.Object({ stack: t.Any(), }), - } + }, ) .post( "/stop", @@ -115,22 +120,22 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Stopped Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} stopped successfully` + `Stack ${body.stack} stopped successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error stopping stack" + "Error stopping stack", ); } }, { - detail: { tags: ["Stacks"] }, + detail: { tags: ["Stacks"], description: "Stop the specified Stack" }, body: t.Object({ stack: t.Any(), }), - } + }, ) .post( "/restart", @@ -143,22 +148,22 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Restarted Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} restarted successfully` + `Stack ${body.stack} restarted successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error restarting stack" + "Error restarting stack", ); } }, { - detail: { tags: ["Stacks"] }, + detail: { tags: ["Stacks"], description: "Restart a whole Stack" }, body: t.Object({ stack: t.Any(), }), - } + }, ) .post( "/pull-images", @@ -171,52 +176,63 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Pulled Stack images (${body.stack})`); return responseHandler.ok( set, - `Images for stack ${body.stack} pulled successfully` + `Images for stack ${body.stack} pulled successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error pulling images" + "Error pulling images", ); } }, { - detail: { tags: ["Stacks"] }, + detail: { + tags: ["Stacks"], + description: "Runs `docker compose pull` on the provided Stack", + }, body: t.Object({ stack: t.Any(), }), - } + }, ) .get( "/status", async ({ set, query }) => { try { - if (!query.stack_name) { - throw new Error("Stack needed"); + let status; + let res = {}; + if (query.stack_name) { + status = await getStackStatus(query.stack_name); + res = responseHandler.ok( + set, + `Stack ${query.stack_name} status retrieved successfully`, + ); + logger.info("Fetched Stack status"); + } else { + status = await getAllStacksStatus(); + res = responseHandler.ok(set, "Fetched all Stack's status"); + logger.info("Fetched all Stack status"); } - logger.debug(query.stack_name); - const status = await getStackStatus(query.stack_name); - const res = responseHandler.ok( - set, - `Stack ${query.stack_name} status retrieved successfully` - ); - logger.info("Fetched Stack status"); return { ...res, status: status }; } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error getting stack status" + "Error getting stack status", ); } }, { - detail: { tags: ["Stacks"] }, + detail: { + tags: ["Stacks"], + description: + "Fetches the current status of all containers for a specific Stack or if no Stack name is provided, for all Stacks", + }, query: t.Object({ stack_name: t.Any(), }), - } + }, ) .get( "/", @@ -229,11 +245,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stacks" + "Error getting stacks", ); } }, { - detail: { tags: ["Stacks"] }, - } + detail: { + tags: ["Stacks"], + description: "Returns an Array of Stack-config-objects", + }, + }, ); diff --git a/src/typings/database.ts b/src/typings/database.ts index c9b15d31..cebda604 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -1,11 +1,3 @@ -interface backend_log_entries { - timestamp: string; - level: string; - message: string; - file: string; - line: number; -} - interface config { keep_data_for: number; fetching_interval: number; @@ -23,4 +15,4 @@ interface stacks_config { image_updates: boolean; } -export type { backend_log_entries, config, stacks_config }; +export type { config, stacks_config }; From 68a6461843c7a39f3b8c7cf5981bcd205b326462 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 12:01:12 +0100 Subject: [PATCH 196/369] Feat: Docker image workflow --- .dockerignore | 6 +++- .github/workflows/docker.yaml | 68 +++++++++++++++++++++++++++++++++++ docker/Dockerfile | 40 ++++++++++++++++++++- package.json | 1 - 4 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/docker.yaml diff --git a/.dockerignore b/.dockerignore index 1e140910..db08b7e1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,8 @@ *.db* /stacks /node_modules -*.md \ No newline at end of file +*.md +/docker +*.dot +*.mmd +*.lock diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 00000000..bc5690d8 --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,68 @@ +name: Docker Build and Test Workflow + +on: + push: + branches: ["**"] + release: + types: [published, prereleased] + +jobs: + test: + name: Test Build on Push + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build image for testing + uses: docker/build-push-action@v4 + with: + context: . + load: true + # build only for current architecture (usually linux/amd64) to enable local loading + platforms: linux/amd64 + tags: dockstatapi:test + + release: + name: Build and Push Docker Image on Release + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PAT }} + + - name: Determine image tag + id: tag + run: | + TAG=${GITHUB_REF##*/} + if [ "${{ github.event.release.prerelease }}" = "true" ]; then + TAG="${TAG}-rc" + fi + echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV + echo "Using tag: $TAG" + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64,linux/arm/v7 + tags: ghcr.io/${{ github.repository_owner }}/dockstatapi:${{ env.IMAGE_TAG }} diff --git a/docker/Dockerfile b/docker/Dockerfile index fdd42344..64b9ba4e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,3 +1,41 @@ -FROM oven/bun AS base +ARG BUILD_DATE +ARG VCS_REF +FROM oven/bun:alpine AS base WORKDIR /base + +COPY package.json bun.lock ./ +RUN bun install -p + +COPY . . + +FROM oven/bun:alpine AS production +WORKDIR /DockStatAPI + +LABEL org.opencontainers.image.title="DockStatAPI" \ + org.opencontainers.image.description="A Dockerized DockStatAPI built with Bun on Alpine Linux." \ + org.opencontainers.image.version="1.0.0" \ + org.opencontainers.image.authors="Your Name " \ + org.opencontainers.image.vendor="Your Company" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.created=$BUILD_DATE \ + org.opencontainers.image.revision=$VCS_REF + +RUN apk add --no-cache curl + +HEALTHCHECK --timeout=30s --start-period=5s --retries=3 \ + CMD curl --fail http://localhost:3000/health || exit 1 + +VOLUME [ "/DockStatAPI/src/plugins" ] + +ENV NODE_ENV=production +ENV LOG_LEVEL=info + +EXPOSE 3000 + +COPY --from=base /base /DockStatAPI + +RUN adduser -D DockStatAPI && chown -R DockStatAPI:DockStatAPI /DockStatAPI +USER DockStatAPI + +ENTRYPOINT [ "bun", "run", "src/index.ts" ] diff --git a/package.json b/package.json index 3426d545..c3f1792d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "version": "2.1.0", "scripts": { "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", - "start:linux": "NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", "build": "bun build --target bun src/index.ts --outdir ./dist", From c341826d2c0b5ea31f3a4972c3338c0075ebecea Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 12:06:54 +0100 Subject: [PATCH 197/369] Fix: Adjust Dockerfile location --- .github/workflows/docker.yaml | 2 ++ docker/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index bc5690d8..9fb1724f 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -24,6 +24,7 @@ jobs: uses: docker/build-push-action@v4 with: context: . + file: docker/Dockerfile load: true # build only for current architecture (usually linux/amd64) to enable local loading platforms: linux/amd64 @@ -63,6 +64,7 @@ jobs: uses: docker/build-push-action@v4 with: context: . + file: docker/Dockerfile push: true platforms: linux/amd64,linux/arm64,linux/arm/v7 tags: ghcr.io/${{ github.repository_owner }}/dockstatapi:${{ env.IMAGE_TAG }} diff --git a/docker/Dockerfile b/docker/Dockerfile index 64b9ba4e..21dbcac5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,7 +4,7 @@ ARG VCS_REF FROM oven/bun:alpine AS base WORKDIR /base -COPY package.json bun.lock ./ +COPY package.json ./ RUN bun install -p COPY . . From 1d96562aab612fedc579cd270fa93e20df78a57f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 12:10:22 +0100 Subject: [PATCH 198/369] Fix: Adjust Login Code --- .github/workflows/docker.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 9fb1724f..6d0c3e08 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -12,16 +12,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build image for testing - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: context: . file: docker/Dockerfile @@ -35,20 +35,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} - password: ${{ secrets.GHCR_PAT }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Determine image tag id: tag @@ -61,7 +61,7 @@ jobs: echo "Using tag: $TAG" - name: Build and push Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: context: . file: docker/Dockerfile From 58b0bd578ba6ffa8bfaeba277737996c6e1a5ed2 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 12:16:13 +0100 Subject: [PATCH 199/369] Fix: Repository name must be lowercase :sob: --- .github/workflows/docker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 6d0c3e08..259d483b 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -67,4 +67,4 @@ jobs: file: docker/Dockerfile push: true platforms: linux/amd64,linux/arm64,linux/arm/v7 - tags: ghcr.io/${{ github.repository_owner }}/dockstatapi:${{ env.IMAGE_TAG }} + tags: ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }} From d861dc624b36643d6f758aa5dd374346ddf08346 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 12:19:58 +0100 Subject: [PATCH 200/369] Fix: Bun alpine image does not support arm/v7 --- .github/workflows/docker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 259d483b..51f618af 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -66,5 +66,5 @@ jobs: context: . file: docker/Dockerfile push: true - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64 tags: ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }} From 2149866caccfb9803453bcf4c0e054cc2897c385 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 12:23:32 +0100 Subject: [PATCH 201/369] Fix: Add permissions --- .github/workflows/docker.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 51f618af..782a2dca 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -6,6 +6,10 @@ on: release: types: [published, prereleased] +permissions: + contents: read + packages: write + jobs: test: name: Test Build on Push From 5015d83fa2b611b818f4045106397a7f21a943f5 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 12:32:20 +0100 Subject: [PATCH 202/369] Fix: Adjustment to Dockerfile --- docker/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 21dbcac5..da820571 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -14,10 +14,10 @@ WORKDIR /DockStatAPI LABEL org.opencontainers.image.title="DockStatAPI" \ org.opencontainers.image.description="A Dockerized DockStatAPI built with Bun on Alpine Linux." \ - org.opencontainers.image.version="1.0.0" \ - org.opencontainers.image.authors="Your Name " \ - org.opencontainers.image.vendor="Your Company" \ - org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.version="3.0.0" \ + org.opencontainers.image.authors="info@itsnik.de" \ + org.opencontainers.image.vendor="Its4Nik" \ + org.opencontainers.image.licenses="CC BY-NC 4.0" \ org.opencontainers.image.created=$BUILD_DATE \ org.opencontainers.image.revision=$VCS_REF From d54b56b1b6043ee106cc6a46675eab72af59112b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 13:26:22 +0100 Subject: [PATCH 203/369] Feat: Info Route --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c3f1792d..3b4498a0 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "license": "CC BY-NC 4.0", "contributors": [], "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", - "version": "2.1.0", + "version": "3.0.0", "scripts": { "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", From ede35bacfbf7a2e753d91e4d558137539253a66d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 19 Mar 2025 14:50:45 +0100 Subject: [PATCH 204/369] Feat: Add utils route(s) --- src/routes/utils.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/routes/utils.ts diff --git a/src/routes/utils.ts b/src/routes/utils.ts new file mode 100644 index 00000000..3afbf35b --- /dev/null +++ b/src/routes/utils.ts @@ -0,0 +1,45 @@ +import { Elysia, t } from "elysia"; +import { + version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, +} from "~/core/utils/package-json"; +import { responseHandler } from "~/core/utils/respone-handler"; + +export const utilRoutes = new Elysia({ prefix: "/utils" }).get( + "/info", + async ({ set }) => { + try { + set.status = 200; + return { + version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + }; + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error getting DockStatAPI information", + ); + } + }, + { + detail: { + tags: ["Utils"], + description: "Shows general information about DockStatAPI", + }, + }, +); From b48d6167bda0fa0b869adb49b834a2e004048958 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 01:37:46 +0100 Subject: [PATCH 205/369] Feat: Live Log endpoint and adjustments --- bun.lock | 2 + src/core/utils/logger.ts | 23 ++- src/index.ts | 12 +- src/middleware/auth.ts | 10 +- src/routes/docker-websocket.ts | 261 +++++++++------------------------ src/routes/live-logs.ts | 29 ++++ src/typings/websocket.ts | 12 +- 7 files changed, 147 insertions(+), 202 deletions(-) create mode 100644 src/routes/live-logs.ts diff --git a/bun.lock b/bun.lock index 9a3a82fc..b8dfca8e 100644 --- a/bun.lock +++ b/bun.lock @@ -388,6 +388,8 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], "easy-table/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 8c321ada..7e3dd566 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -3,6 +3,10 @@ import path from "path"; import chalk, { ChalkInstance } from "chalk"; import { dbFunctions } from "../database/repository"; import wrapAnsi from "wrap-ansi"; +import { logToClients } from "~/routes/live-logs"; +import { logStreamData } from "~/typings/websocket"; + +const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; // Change to false here if dont want the spacing on a wrapped line const padNewlines: boolean = true; @@ -80,6 +84,16 @@ export const logger = createLogger({ message = `[ ${chalk.greenBright("Plugin")} ] ${message}`; } + const logStreamData: logStreamData = { + timestamp: timestamp as string, + level: level as string, + message: (message as string).replace(ansiRegex, ""), + file: file as string, + line: line as number, + }; + + logToClients(logStreamData); + const paddedLevel = level.toUpperCase().padEnd(5); const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); const coloredContext = chalk.cyan(`${file as string}:${line as number}`); @@ -87,7 +101,7 @@ export const logger = createLogger({ if (process.env.NODE_ENV !== "dev") { return `${coloredLevel} [ ${coloredTimestamp} ] - ${chalk.gray( - message, + message )} - [ ${coloredContext} ]`; } @@ -95,16 +109,15 @@ export const logger = createLogger({ const prefixLength = prefix.length; const formattedMessage = formatTerminalMessage( message as string, - prefixLength, + prefixLength ); - const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; try { dbFunctions.addLogEntry( (level as string).replace(ansiRegex, ""), (message as string).replace(ansiRegex, ""), (file as string).replace(ansiRegex, ""), - line as number, + line as number ); } catch (error) { // Use console.error to avoid recursive logging @@ -113,7 +126,7 @@ export const logger = createLogger({ } return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; - }), + }) ), transports: [new transports.Console()], }); diff --git a/src/index.ts b/src/index.ts index ff6b763e..ad16d1ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,8 @@ import trpcRouter from "~/core/trpc"; import { config } from "./typings/database"; import { validateApiKey } from "./middleware/auth"; import { monitorDockerEvents } from "./core/docker/monitor"; +import { liveLogs } from "./routes/live-logs"; +import { utilRoutes } from "./routes/utils"; console.log(""); dbFunctions.init(); @@ -66,7 +68,7 @@ const DockStatAPI = new Elysia() }, ], }, - }), + }) ) .onBeforeHandle(async (context) => { const { path, request, set } = context; @@ -91,6 +93,8 @@ const DockStatAPI = new Elysia() .use(dockerWebsocketRoutes) .use(apiConfigRoutes) .use(stackRoutes) + .use(utilRoutes) + .use(liveLogs) .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) .onError(({ code, set, path }) => { if (code === "NOT_FOUND") { @@ -115,7 +119,7 @@ async function startServer() { if (apiKey === "changeme") { logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" ); } @@ -123,10 +127,10 @@ async function startServer() { console.log("----- [ ############## ]"); logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + `Swagger API Documentation available at http://${hostname}:${port}/swagger` ); logger.info( - `tRPC Endpoint available at: http://${hostname}:${port}/trpc`, + `tRPC Endpoint available at: http://${hostname}:${port}/trpc` ); }); } catch (error) { diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index ac0c8609..5ec3f19a 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -16,7 +16,7 @@ export async function hashApiKey(apiKey: string): Promise { async function validateApiKeyHash( providedKey: string, - storedHash: string, + storedHash: string ): Promise { logger.debug("Validating API key hash"); try { @@ -30,7 +30,7 @@ async function validateApiKeyHash( } async function getApiKeyFromDb( - apiKey: string, + apiKey: string ): Promise<{ hash: string } | null> { const dbApiKey = (dbFunctions.getConfig() as config[])[0].api_key; logger.debug(`Querying database for API key: ${apiKey}`); @@ -41,9 +41,11 @@ async function getApiKeyFromDb( export async function validateApiKey(request: Request, set: set) { const apiKey = request.headers.get("x-api-key"); - logger.debug(`API key validation initiated`); if (process.env.NODE_ENV != "production") { + logger.warn( + "API Key validation deactivated, since running in development mode" + ); return { apiKey }; } else if (!apiKey) { logger.error(`API key missing from request ${request.url}`); @@ -51,6 +53,8 @@ export async function validateApiKey(request: Request, set: set) { return { error: "API key required" }; } + logger.debug(`API key validation initiated`); + try { const dbRecord = await getApiKeyFromDb(apiKey); diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 4b4fae7a..5520579c 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -1,6 +1,5 @@ -import type { StatusMap } from "elysia"; import { Elysia } from "elysia"; -import type { HTTPHeaders } from "elysia/dist/types"; +import type { ElysiaWS } from "elysia/dist/ws"; import { dbFunctions } from "~/core/database/repository"; import { getDockerClient } from "~/core/docker/client"; import { @@ -9,236 +8,122 @@ import { } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/respone-handler"; -import type { DockerHost } from "~/typings/docker"; import split2 from "split2"; import type { Readable } from "stream"; -import type { streams } from "~/typings/websocket"; -interface ExtendedWebSocket extends WebSocket { - isOpen: boolean; - streams: any[]; - heartbeat: NodeJS.Timeout | null; -} - -const set: { headers: HTTPHeaders; status?: number | keyof StatusMap } = { - headers: {}, -}; +const activeDockerConnections = new Set>(); +const connectionStreams = new Map< + ElysiaWS, + Array<{ statsStream: Readable; splitStream: ReturnType }> +>(); export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( "/stats", { - async open(socket) { - socket.send(JSON.stringify({ message: "Connection established" })); - let hosts: DockerHost[]; - - (socket as unknown as ExtendedWebSocket).isOpen = true; - (socket as unknown as ExtendedWebSocket).streams = []; - (socket as unknown as ExtendedWebSocket).heartbeat = null; // Add heartbeat reference + async open(ws) { + activeDockerConnections.add(ws); + connectionStreams.set(ws, []); - logger.info(`Opened WebSocket (${socket.id})`); + ws.send(JSON.stringify({ message: "Connection established" })); + logger.info(`New Docker WebSocket established (${ws.id})`); try { - hosts = dbFunctions.getDockerHosts(); - logger.debug( - `Retrieved ${hosts.length} docker host(s) from the database`, - ); - } catch (error: unknown) { - const errResponse = responseHandler.error( - set, - (error as Error).message, - "Failed to retrieve Docker hosts", - 500, - ); - logger.error( - `Error retrieving Docker hosts: ${(error as Error).message}`, - ); - socket.send(JSON.stringify(errResponse)); - return; - } - - // Add heartbeat using WebSocket protocol-level ping - (socket as any).heartbeat = setInterval(() => { - if (!(socket as unknown as ExtendedWebSocket).isOpen) { - clearInterval((socket as any).heartbeat); - return; - } - socket.ping(); // Use WebSocket protocol ping - }, 30000); - - for (const host of hosts) { - if (!(socket as unknown as ExtendedWebSocket).isOpen) { - break; - } + const hosts = dbFunctions.getDockerHosts(); + logger.debug(`Retrieved ${hosts.length} docker host(s)`); - logger.debug(`Processing host: ${host.name}`); + for (const host of hosts) { + if (ws.readyState !== 1) { + break; + } - try { const docker = getDockerClient(host); await docker.ping(); - logger.debug(`Ping successful for host: ${host.name}`); - logger.debug(`Listing containers for host: ${host.name}`); const containers = await docker.listContainers(); - logger.debug( - `Found ${containers.length} container(s) on host ${host.name}`, - ); + logger.debug(`Found ${containers.length} containers on ${host.name}`); for (const containerInfo of containers) { - if (!(socket as unknown as ExtendedWebSocket).isOpen) { + if (ws.readyState !== 1) { break; } - logger.debug( - `Processing container ${containerInfo.Id} on host ${host.name}`, - ); const container = docker.getContainer(containerInfo.Id); - try { - logger.debug( - `Starting stats stream for container ${containerInfo.Id} on host ${host.name}`, - ); - const statsStream = (await container.stats({ - stream: true, - })) as Readable; - const splitStream = split2(); - - // Store both streams for cleanup - (socket as unknown as ExtendedWebSocket).streams.push({ - statsStream, - splitStream, - }); - - // Handle stream lifecycle - statsStream - .on("close", () => { - logger.debug(`Stats stream closed for ${containerInfo.Id}`); - splitStream.destroy(); - }) - .on("end", () => { - logger.debug(`Stats stream ended for ${containerInfo.Id}`); - splitStream.destroy(); - }); - - statsStream - .pipe(splitStream) - .on("data", (line: string) => { - // 1 = OPEN state - if (socket.readyState !== 1) { - return; - } - if (!line) { - return; - } - try { - const stats = JSON.parse(line); - const cpuUsage = calculateCpuPercent(stats); - const memoryUsage = calculateMemoryUsage(stats); - - const data = { + const statsStream = (await container.stats({ + stream: true, + })) as Readable; + const splitStream = split2(); + + connectionStreams.get(ws)?.push({ statsStream, splitStream }); + + statsStream + .on("close", () => splitStream.destroy()) + .pipe(splitStream) + .on("data", (line: string) => { + if (ws.readyState !== 1 || !line) return; + try { + const stats = JSON.parse(line); + ws.send( + JSON.stringify({ id: containerInfo.Id, hostId: host.name, name: containerInfo.Names[0].replace(/^\//, ""), image: containerInfo.Image, status: containerInfo.Status, state: containerInfo.State, - cpuUsage, - memoryUsage, - }; - socket.send(JSON.stringify(data)); - } catch (parseErr: any) { - logger.error( - `Failed to parse stats for container ${containerInfo.Id} on host ${host.name}: ${parseErr.message}`, - ); - } - }) - .on("error", (err: Error) => { - logger.error( - `Stats stream error for container ${containerInfo.Id} on host ${host.name}: ${err.message}`, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + }) ); - if (socket.readyState === 1) { - socket.send( - JSON.stringify({ - hostId: host.name, - containerId: containerInfo.Id, - error: `Stats stream error for container ${containerInfo.Id} on host ${host.name}`, - }), - ); - } - statsStream.destroy(); - }); - } catch (streamErr: any) { - const errMsg = `Failed to start stats stream for container ${containerInfo.Id}`; - logger.error( - `Failed to start stats stream for container ${containerInfo.Id} on host ${host.name}: ${streamErr.message}`, - ); - if (socket.readyState === 1) { - socket.send( + } catch (error) { + logger.error(`Parse error: ${error}`); + } + }) + .on("error", (error: Error) => { + logger.error(`Stream error: ${error}`); + statsStream.destroy(); + ws.send( JSON.stringify({ hostId: host.name, containerId: containerInfo.Id, - error: errMsg, - }), + error: `Stats stream error: ${error}`, + }) ); - } - } - } - } catch (err: any) { - logger.error( - `Failed to list containers for host ${host.name}: ${err.message}`, - ); - const errResponse = responseHandler.error( - set, - err.message, - `Failed to list containers for host ${host.name}`, - 500, - ); - if (socket.readyState === 1) { - socket.send( - JSON.stringify({ - hostId: host.name, - error: errResponse.error, - }), - ); + }); } } + } catch (error) { + logger.error(`Connection error: ${error}`); + ws.send( + JSON.stringify( + responseHandler.error( + { headers: {} }, + error as string, + "Docker connection failed", + 500 + ) + ) + ); } }, - message(_, message) { - if (message === "pong") { - return; - } + message(ws, message) { + if (message === "pong") ws.pong(); }, - close(socket, code, reason) { - logger.info(`Closing SplitStream and WebSocket (${socket.id})`); - const wasOpen = (socket as unknown as ExtendedWebSocket).isOpen; - (socket as unknown as ExtendedWebSocket).isOpen = false; - - // Immediate heartbeat cleanup - clearInterval((socket as any).heartbeat); + close(ws) { + logger.info(`Closing connection ${ws.id}`); + activeDockerConnections.delete(ws); - // Force-close streams using destructor pattern - const streams: streams[] = - (socket as unknown as ExtendedWebSocket).streams || []; + const streams = connectionStreams.get(ws) || []; streams.forEach(({ statsStream, splitStream }) => { try { - // Immediate pipeline breakdown statsStream.unpipe(splitStream); - statsStream.destroy(new Error("WebSocket closed")); - splitStream.destroy(new Error("WebSocket closed")); - - // Remove all potential listeners - statsStream.removeAllListeners(); - splitStream.removeAllListeners(); - } catch (err) { - logger.error(`Stream cleanup error: ${err}`); + statsStream.destroy(); + splitStream.destroy(); + } catch (error) { + logger.error(`Cleanup error: ${error}`); } }); - - if (wasOpen) { - logger.info( - `Closed WebSocket (${socket.id}) - Code: ${code} - Reason: ${reason}`, - ); - } + connectionStreams.delete(ws); }, - }, + } ); diff --git a/src/routes/live-logs.ts b/src/routes/live-logs.ts new file mode 100644 index 00000000..1232ce5a --- /dev/null +++ b/src/routes/live-logs.ts @@ -0,0 +1,29 @@ +import { Elysia } from "elysia"; +import type { ElysiaWS } from "elysia/dist/ws"; +import { logger } from "~/core/utils/logger"; +import type { logStreamData } from "~/typings/websocket"; + +const activeConnections = new Set>(); + +export const liveLogs = new Elysia({ prefix: "/logs" }).ws("/ws", { + open(ws) { + activeConnections.add(ws); + ws.send({ message: "Connection established" }); + logger.info(`New Logs WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Logs WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, +}); + +export function logToClients(data: logStreamData) { + activeConnections.forEach((ws) => { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + }); +} diff --git a/src/typings/websocket.ts b/src/typings/websocket.ts index a9712473..59f5e3a3 100644 --- a/src/typings/websocket.ts +++ b/src/typings/websocket.ts @@ -1,4 +1,4 @@ -import type { Readable } from "stream"; +import type { Readable, Transform } from "stream"; import type internal from "stream"; interface streams { @@ -6,4 +6,12 @@ interface streams { splitStream: internal.Transform; } -export { streams }; +interface logStreamData { + timestamp: string; + level: string; + message: string; + file: string; + line: number; +} + +export { streams, logStreamData }; From 74459313d385ce3c75b79a5ce71190a6a17ab41e Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 21 Mar 2025 00:38:15 +0000 Subject: [PATCH 206/369] Update dependency graphs --- dependency-graph.dot | 12 +- dependency-graph.mmd | 256 +++++------ dependency-graph.svg | 994 +++++++++++++++++++++++-------------------- 3 files changed, 668 insertions(+), 594 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 8759b82b..f6d55f95 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -104,6 +104,8 @@ strict digraph "dependency-cruiser output"{ "src/core/utils/change-me-checker.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/logger.ts" [label= tooltip="logger.ts" URL="src/core/utils/logger.ts" fillcolor="#ddfeff"] } } } "src/core/utils/logger.ts" -> "src/core/database/repository.ts" [arrowhead="normalnoneodot"] + "src/core/utils/logger.ts" -> "src/routes/live-logs.ts" [arrowhead="normalnoneodot"] + "src/core/utils/logger.ts" -> "src/typings/websocket.ts" "src/core/utils/logger.ts" -> "path" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/package-json.ts" [label= tooltip="package-json.ts" URL="src/core/utils/package-json.ts" fillcolor="#ddfeff"] } } } "src/core/utils/package-json.ts" -> "package.json" @@ -113,7 +115,9 @@ strict digraph "dependency-cruiser output"{ subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } "src/index.ts" -> "src/core/docker/monitor.ts" "src/index.ts" -> "src/middleware/auth.ts" + "src/index.ts" -> "src/routes/live-logs.ts" "src/index.ts" -> "src/routes/stacks.ts" + "src/index.ts" -> "src/routes/utils.ts" "src/index.ts" -> "src/typings/database.ts" "src/index.ts" -> "src/core/database/repository.ts" "src/index.ts" -> "src/core/docker/scheduler.ts" @@ -156,9 +160,10 @@ strict digraph "dependency-cruiser output"{ "src/routes/docker-websocket.ts" -> "src/core/utils/calculations.ts" "src/routes/docker-websocket.ts" -> "src/core/utils/logger.ts" "src/routes/docker-websocket.ts" -> "src/core/utils/respone-handler.ts" - "src/routes/docker-websocket.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/routes/docker-websocket.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] "src/routes/docker-websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/live-logs.ts" [label= tooltip="live-logs.ts" URL="src/routes/live-logs.ts" fillcolor="#ddfeff"] } } + "src/routes/live-logs.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] + "src/routes/live-logs.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/logs.ts" [label= tooltip="logs.ts" URL="src/routes/logs.ts" fillcolor="#ddfeff"] } } "src/routes/logs.ts" -> "src/core/database/repository.ts" "src/routes/logs.ts" -> "src/core/utils/logger.ts" @@ -167,6 +172,9 @@ strict digraph "dependency-cruiser output"{ "src/routes/stacks.ts" -> "src/core/stacks/controller.ts" "src/routes/stacks.ts" -> "src/core/utils/logger.ts" "src/routes/stacks.ts" -> "src/core/utils/respone-handler.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/utils.ts" [label= tooltip="utils.ts" URL="src/routes/utils.ts" fillcolor="#ddfeff"] } } + "src/routes/utils.ts" -> "src/core/utils/package-json.ts" + "src/routes/utils.ts" -> "src/core/utils/respone-handler.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/database.ts" [label= tooltip="database.ts" URL="src/typings/database.ts" fillcolor="#ddfeff"] } } subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker-compose.ts" [label= tooltip="docker-compose.ts" URL="src/typings/docker-compose.ts" fillcolor="#ddfeff"] } } subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker.ts" [label= tooltip="docker.ts" URL="src/typings/docker.ts" fillcolor="#ddfeff"] } } diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 0e9f5167..01974834 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -11,98 +11,104 @@ subgraph 0["src"] subgraph 2["core"] subgraph 3["docker"] 4["monitor.ts"] -K["client.ts"] -U["scheduler.ts"] -V["store-host-stats.ts"] -X["store-container-stats.ts"] +O["client.ts"] +10["scheduler.ts"] +11["store-host-stats.ts"] +13["store-container-stats.ts"] end subgraph 6["plugins"] 7["plugin-manager.ts"] -Z["loader.ts"] +15["loader.ts"] end subgraph 9["utils"] A["logger.ts"] -T["respone-handler.ts"] -Y["calculations.ts"] -11["change-me-checker.ts"] -19["package-json.ts"] +W["respone-handler.ts"] +Y["package-json.ts"] +14["calculations.ts"] +17["change-me-checker.ts"] end subgraph C["database"] D["repository.ts"] F["helper.ts"] end -subgraph Q["stacks"] -R["controller.ts"] +subgraph T["stacks"] +U["controller.ts"] end -subgraph 13["trpc"] -14["index.ts"] -15["router.ts"] -subgraph 16["procedures"] -17["api-config.procedure.ts"] -1B["docker-manager.procedure.ts"] -1C["docker-stats.procedure.ts"] -1D["logs.procedure.ts"] -1E["stacks.procedure.ts"] +subgraph 19["trpc"] +1A["index.ts"] +1B["router.ts"] +subgraph 1C["procedures"] +1D["api-config.procedure.ts"] +1F["docker-manager.procedure.ts"] +1G["docker-stats.procedure.ts"] +1H["logs.procedure.ts"] +1I["stacks.procedure.ts"] end -18["trpc.ts"] +1E["trpc.ts"] end end subgraph G["typings"] H["database.ts"] I["docker.ts"] -J["plugin.ts"] -N["elysiajs.ts"] -S["docker-compose.ts"] -W["dockerode.ts"] -1K["websocket.ts"] +L["websocket.ts"] +N["plugin.ts"] +R["elysiajs.ts"] +V["docker-compose.ts"] +12["dockerode.ts"] end -subgraph L["middleware"] -M["auth.ts"] +subgraph J["routes"] +K["live-logs.ts"] +S["stacks.ts"] +X["utils.ts"] +1J["api-config.ts"] +1K["docker-manager.ts"] +1L["docker-stats.ts"] +1M["docker-websocket.ts"] +1N["logs.ts"] end -subgraph O["routes"] -P["stacks.ts"] -1F["api-config.ts"] -1G["docker-manager.ts"] -1H["docker-stats.ts"] -1I["docker-websocket.ts"] -1L["logs.ts"] +subgraph P["middleware"] +Q["auth.ts"] end end 5["bun"] 8["events"] B["path"] E["bun:sqlite"] -subgraph 10["fs"] -12["promises"] +M["stream"] +Z["package.json"] +subgraph 16["fs"] +18["promises"] end -1A["package.json"] -1J["stream"] 1-->4 -1-->M -1-->P +1-->Q +1-->K +1-->S +1-->X 1-->H 1-->D -1-->U -1-->Z -1-->14 +1-->10 +1-->15 +1-->1A 1-->A -1-->1F -1-->1G -1-->1H -1-->1I +1-->1J +1-->1K 1-->1L +1-->1M +1-->1N 4-->7 4-->D -4-->K +4-->O 4-->A 4-->I 4-->I 4-->5 7-->A 7-->I -7-->J +7-->N 7-->8 A-->D +A-->K +A-->L A-->B D-->F D-->A @@ -110,99 +116,101 @@ D-->H D-->I D-->E F-->A -J-->I K-->A -K-->I -M-->D -M-->A -M-->H -M-->N -P-->D -P-->R -P-->A -P-->T -R-->D -R-->A -R-->H -R-->S -T-->A -T-->N +K-->L +L-->M +N-->I +O-->A +O-->I +Q-->D +Q-->A +Q-->H +Q-->R +S-->D +S-->U +S-->A +S-->W U-->D -U-->V -U-->X U-->A U-->H -V-->D -V-->K -V-->A -V-->I -V-->W -X-->D -X-->K +U-->V +W-->A +W-->R X-->Y -Z-->11 -Z-->A -Z-->7 -Z-->10 -Z-->B +X-->W +Y-->Z +10-->D +10-->11 +10-->13 +10-->A +10-->H +11-->D +11-->O 11-->A +11-->I 11-->12 -14-->15 +13-->D +13-->O +13-->14 15-->17 -15-->1B -15-->1C -15-->1D -15-->1E -15-->18 -17-->18 -17-->D +15-->A +15-->7 +15-->16 +15-->B 17-->A -17-->19 -17-->H -19-->1A -1B-->18 -1B-->D -1B-->A -1C-->18 -1C-->D -1C-->K -1C-->Y -1C-->A -1C-->I -1C-->W -1D-->18 +17-->18 +1A-->1B +1B-->1D +1B-->1F +1B-->1G +1B-->1H +1B-->1I +1B-->1E +1D-->1E 1D-->D 1D-->A -1E-->18 -1E-->D -1E-->R -1E-->A +1D-->Y +1D-->H +1F-->1E 1F-->D -1F-->7 1F-->A -1F-->19 -1F-->T -1F-->M -1F-->H +1G-->1E 1G-->D +1G-->O +1G-->14 1G-->A -1G-->T +1G-->I +1G-->12 +1H-->1E 1H-->D -1H-->K -1H-->Y 1H-->A -1H-->T -1H-->I -1H-->W +1I-->1E 1I-->D -1I-->K -1I-->Y +1I-->U 1I-->A -1I-->T -1I-->I -1I-->1K -1I-->1J -1K-->1J +1J-->D +1J-->7 +1J-->A +1J-->Y +1J-->W +1J-->Q +1J-->H +1K-->D +1K-->A +1K-->W 1L-->D +1L-->O +1L-->14 1L-->A +1L-->W +1L-->I +1L-->12 +1M-->D +1M-->O +1M-->14 +1M-->A +1M-->W +1M-->M +1N-->D +1N-->A diff --git a/dependency-graph.svg b/dependency-graph.svg index 7ea0c591..89342e89 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,70 +4,70 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/trpc - -trpc + +trpc cluster_src/core/trpc/procedures - -procedures + +procedures cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings @@ -78,8 +78,8 @@ bun - -bun + +bun @@ -87,8 +87,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -96,8 +96,8 @@ events - -events + +events @@ -105,8 +105,8 @@ fs - -fs + +fs @@ -114,8 +114,8 @@ fs/promises - -promises + +promises @@ -123,8 +123,8 @@ package.json - -package.json + +package.json @@ -132,8 +132,8 @@ path - -path + +path @@ -141,8 +141,8 @@ src/core/database/helper.ts - -helper.ts + +helper.ts @@ -150,63 +150,95 @@ src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + src/core/database/repository.ts - -repository.ts + +repository.ts src/core/utils/logger.ts->src/core/database/repository.ts - - - - + + + + + + + +src/routes/live-logs.ts + + +live-logs.ts + + + + + +src/core/utils/logger.ts->src/routes/live-logs.ts + + + + + + + +src/typings/websocket.ts + + +websocket.ts + + + + + +src/core/utils/logger.ts->src/typings/websocket.ts + + src/core/database/repository.ts->bun:sqlite - - + + src/core/database/repository.ts->src/core/database/helper.ts - - - - + + + + src/core/database/repository.ts->src/core/utils/logger.ts - - - - + + + + @@ -220,8 +252,8 @@ src/core/database/repository.ts->src/typings/database.ts - - + + @@ -235,197 +267,197 @@ src/core/database/repository.ts->src/typings/docker.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->bun - - + + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->src/core/database/repository.ts - - + + src/core/docker/monitor.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + src/core/plugins/plugin-manager.ts->events - - + + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + src/typings/plugin.ts - -plugin.ts + +plugin.ts src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/repository.ts - - + + src/core/docker/scheduler.ts->src/typings/database.ts - + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + @@ -439,127 +471,127 @@ src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + src/core/utils/change-me-checker.ts->fs/promises - - + + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts->src/core/database/repository.ts - - + + src/core/stacks/controller.ts->src/typings/database.ts - + @@ -574,15 +606,15 @@ src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + src/core/trpc/index.ts - -index.ts + +index.ts @@ -590,673 +622,699 @@ src/core/trpc/router.ts - -router.ts + +router.ts src/core/trpc/index.ts->src/core/trpc/router.ts - - + + src/core/trpc/procedures/api-config.procedure.ts - -api-config.procedure.ts + +api-config.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - + + src/core/trpc/trpc.ts - -trpc.ts + +trpc.ts src/core/trpc/router.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts - -docker-manager.procedure.ts + +docker-manager.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts - -docker-stats.procedure.ts + +docker-stats.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - + + src/core/trpc/procedures/logs.procedure.ts - -logs.procedure.ts + +logs.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - + + src/core/trpc/procedures/stacks.procedure.ts - -stacks.procedure.ts + +stacks.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - + + - + src/core/utils/package-json.ts->package.json - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + + + + +src/routes/live-logs.ts->src/core/utils/logger.ts + + + + + + + +src/routes/live-logs.ts->src/typings/websocket.ts + + + + + +stream + + +stream + + + + + +src/typings/websocket.ts->stream + + - + src/core/utils/respone-handler.ts - - -respone-handler.ts + + +respone-handler.ts - + src/core/utils/respone-handler.ts->src/core/utils/logger.ts - - + + - + src/typings/elysiajs.ts - - -elysiajs.ts + + +elysiajs.ts - + src/core/utils/respone-handler.ts->src/typings/elysiajs.ts - - + + - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/repository.ts - - + + - + src/index.ts->src/typings/database.ts - + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/core/trpc/index.ts - - + + + + + +src/index.ts->src/routes/live-logs.ts + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + + + + +src/routes/utils.ts + + +utils.ts + + + + + +src/index.ts->src/routes/utils.ts + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/repository.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/repository.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/respone-handler.ts - - + + + + + +src/routes/utils.ts->src/core/utils/package-json.ts + + + + + +src/routes/utils.ts->src/core/utils/respone-handler.ts + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/repository.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->src/core/database/repository.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->src/core/database/repository.ts - - + + - + src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/repository.ts - - - - - -src/routes/docker-websocket.ts->src/typings/docker.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts - - - - - -src/typings/websocket.ts - - -websocket.ts - - - - - -src/routes/docker-websocket.ts->src/typings/websocket.ts - - - - - -stream - - -stream - - + + - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/repository.ts - - - - - -src/typings/websocket.ts->stream - - + + From 81add7460fa79e5bdc9906b0e3951d6928d1a39c Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 01:48:22 +0100 Subject: [PATCH 207/369] Feat: Unit testing --- package.json | 1 + src/core/utils/logger.ts | 18 ++++-- src/index.ts | 3 +- src/tests/cleanup.ts | 7 +++ src/tests/delete.spec.ts | 12 ++++ src/tests/gets.spec.ts | 59 +++++++++++++++++++ src/tests/helper.ts | 119 +++++++++++++++++++++++++++++++++++++++ src/tests/post.spec.ts | 40 +++++++++++++ 8 files changed, 254 insertions(+), 5 deletions(-) create mode 100644 src/tests/cleanup.ts create mode 100644 src/tests/delete.spec.ts create mode 100644 src/tests/gets.spec.ts create mode 100644 src/tests/helper.ts create mode 100644 src/tests/post.spec.ts diff --git a/package.json b/package.json index 3b4498a0..3964ec88 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", "version": "3.0.0", "scripts": { + "test": "bun clean && bun test && bun run ./src/tests/cleanup.ts", "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 7e3dd566..903df1f9 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -9,7 +9,7 @@ import { logStreamData } from "~/typings/websocket"; const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; // Change to false here if dont want the spacing on a wrapped line -const padNewlines: boolean = true; +const padNewlines: boolean = process.env.PAD_NEW_LINES === "true" || true; const fileLineFormat = format((info) => { try { @@ -69,6 +69,7 @@ export const logger = createLogger({ verbose: chalk.cyan.bold, silly: chalk.magenta.bold, task: chalk.cyan.bold, + ut: chalk.hex("#9D00FF"), }; if ((message as string).startsWith("__task__")) { @@ -80,6 +81,11 @@ export const logger = createLogger({ } } + if ((message as string).startsWith("__UT__")) { + message = (message as string).replaceAll("__UT__", "").trimStart(); + level = "ut"; + } + if ((file as string).includes("plugin.ts")) { message = `[ ${chalk.greenBright("Plugin")} ] ${message}`; } @@ -101,7 +107,7 @@ export const logger = createLogger({ if (process.env.NODE_ENV !== "dev") { return `${coloredLevel} [ ${coloredTimestamp} ] - ${chalk.gray( - message + message, )} - [ ${coloredContext} ]`; } @@ -117,7 +123,7 @@ export const logger = createLogger({ (level as string).replace(ansiRegex, ""), (message as string).replace(ansiRegex, ""), (file as string).replace(ansiRegex, ""), - line as number + line as number, ); } catch (error) { // Use console.error to avoid recursive logging @@ -126,7 +132,11 @@ export const logger = createLogger({ } return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; - }) + + const fullMessage = `${coloredLevel} [ ${coloredTimestamp} ] - ${message} - [ ${coloredContext} ]`; + + return formatTerminalMessage(fullMessage, prefixLength); + }), ), transports: [new transports.Console()], }); diff --git a/src/index.ts b/src/index.ts index ad16d1ab..f96da4b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,7 @@ import { utilRoutes } from "./routes/utils"; console.log(""); dbFunctions.init(); -const DockStatAPI = new Elysia() +export const DockStatAPI = new Elysia() .use(staticPlugin()) .use(serverTiming()) .use( @@ -92,6 +92,7 @@ const DockStatAPI = new Elysia() .use(backendLogs) .use(dockerWebsocketRoutes) .use(apiConfigRoutes) + .use(utilRoutes) .use(stackRoutes) .use(utilRoutes) .use(liveLogs) diff --git a/src/tests/cleanup.ts b/src/tests/cleanup.ts new file mode 100644 index 00000000..6965dd40 --- /dev/null +++ b/src/tests/cleanup.ts @@ -0,0 +1,7 @@ +import { dbFunctions } from "~/core/database/repository"; + +console.log(""); +console.log("Deleting `test` Docker host"); +dbFunctions.deleteDockerHost("test"); +console.log("Cleanuing up Database config to default values"); +dbFunctions.updateConfig(5, 7, "changeme"); diff --git a/src/tests/delete.spec.ts b/src/tests/delete.spec.ts new file mode 100644 index 00000000..ee8ad22a --- /dev/null +++ b/src/tests/delete.spec.ts @@ -0,0 +1,12 @@ +import { describe, it } from "bun:test"; +import { runTestCode } from "./helper"; + +describe("DockStatAPI (DELETE)", () => { + it("Delete all Logs /logs", async () => { + await runTestCode("/logs", 200, "DELETE", "{}"); + }); + + it("Delete Logs (Debug) /logs/debug", async () => { + await runTestCode("/logs/debug", 200, "DELETE", "{}"); + }); +}); diff --git a/src/tests/gets.spec.ts b/src/tests/gets.spec.ts new file mode 100644 index 00000000..f211c39f --- /dev/null +++ b/src/tests/gets.spec.ts @@ -0,0 +1,59 @@ +import { describe, it } from "bun:test"; +import { runTestResponse, runTestCode } from "./helper"; +import { + version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, +} from "~/core/utils/package-json"; + +describe("DockStatAPI (GET)", () => { + it("Check Server connection", async () => { + await runTestResponse("/health", '{"status":"healthy"}', "GET"); + }); + + it("Check /docker/containers", async () => { + await runTestCode("/docker/containers", 200, "GET"); + }); + + it("Check /docker/hosts/Localhost", async () => { + await runTestCode("/docker/hosts/Localhost", 200, "GET"); + }); + + it("Check /docker-config/hosts", async () => { + await runTestCode("/docker-config/hosts", 200, "GET"); + }); + + it("Check /logs/", async () => { + await runTestCode("/logs", 200, "GET"); + }); + + it("Check /logs/debug", async () => { + await runTestCode("/logs/debug", 200, "GET"); + }); + + it("Check /config", async () => { + await runTestCode("/config", 200, "GET"); + }); + + it("Check /config/package", async () => { + const expected = { + version, + description, + license, + authorName, + authorEmail, + authorWebsite, + contributors, + dependencies, + devDependencies, + }; + + await runTestResponse("/config/package", JSON.stringify(expected), "GET"); + }); +}); diff --git a/src/tests/helper.ts b/src/tests/helper.ts new file mode 100644 index 00000000..1c773df9 --- /dev/null +++ b/src/tests/helper.ts @@ -0,0 +1,119 @@ +import { expect } from "bun:test"; +import { DockStatAPI } from ".."; +import { logger } from "~/core/utils/logger"; +export const API_KEY = "TestKey"; + +export async function runTestResponse( + path: string, + expected_response: string, + method?: "GET" | "POST" | "DELETE", +) { + if (!method) { + method = "GET"; + } + + const server = "http://localhost:3000"; + const route = `${server}${path}`; + + logger.info(`__UT__ [START] Running test, method: ${method} on ${route}`); + const startTime = Date.now(); + + try { + const request = new Request(route, { + method, + verbose: true, + headers: { + "Content-Type": "application/json", + "x-api-key": API_KEY, + }, + }); + logger.debug( + `__UT__ Request details: ${JSON.stringify({ + url: route, + method, + headers: [...request.headers], + })}`, + ); + + // Get the response + const response = await DockStatAPI.handle(request); + const headers: any = {}; + response.headers.forEach((value, key: any) => { + headers[key] = value; + }); + logger.debug(`__UT__ Received HTTP status: ${response.status}`); + logger.debug(`__UT__ Response headers: ${JSON.stringify(headers)}`); + + // Log the response body as text + const responseText = await response.text(); + const duration = Date.now() - startTime; + logger.debug(`__UT__ Response body: ${responseText}`); + logger.debug(`__UT__ Total Duration: ${duration}ms`); + logger.info(`__UT__ [END] Completed test on ${route}`); + + return expect(responseText).toBe(expected_response); + } catch (error) { + logger.error(`__UT__ Error during test on ${route}: ${error}`); + throw error; + } +} + +export async function runTestCode( + path: string, + expected_code: number, + method?: "GET" | "POST" | "DELETE", + requestBody?: string, +) { + if (!method) { + method = "GET"; + } + + if (!requestBody) { + requestBody = ""; + } + + const server = "http://localhost:3000"; + const route = `${server}${path}`; + + logger.info(`__UT__ [START] Running test, method: ${method} on ${route}`); + const startTime = Date.now(); + + try { + const request = new Request(route, { + method, + verbose: true, + body: requestBody, + headers: { + "Content-Type": "application/json", + "x-api-key": API_KEY, + }, + }); + logger.debug( + `__UT__ Request details: ${JSON.stringify({ + url: route, + method, + headers: [...request.headers], + body: requestBody, + })}`, + ); + + const response = await DockStatAPI.handle(request); + logger.debug(`__UT__ Received HTTP status: ${response.status}`); + + const headers: any = {}; + response.headers.forEach((value, key) => { + headers[key] = value; + }); + + logger.debug(`__UT__ Response headers: ${JSON.stringify(headers)}`); + logger.debug(`__UT__ Response: ${JSON.stringify(response.body)}`); + + const duration = Date.now() - startTime; + logger.debug(`__UT__ Completed test on ${route} (Duration: ${duration}ms)`); + + expect(response.status).toBe(expected_code); + } catch (error) { + logger.error(`__UT__ Error during test on ${route}: ${error}`); + throw error; + } +} diff --git a/src/tests/post.spec.ts b/src/tests/post.spec.ts new file mode 100644 index 00000000..4d780eee --- /dev/null +++ b/src/tests/post.spec.ts @@ -0,0 +1,40 @@ +import { describe, it } from "bun:test"; +import { runTestResponse, runTestCode } from "./helper"; +import { dbFunctions } from "~/core/database/repository"; +import { API_KEY } from "./helper"; + +describe("DockStatAPI (POST)", () => { + it("Check Host adding", async () => { + const body: string = + '{"name":"test","url":"localhost:2375","secure":false}'; + + await runTestCode("/docker-config/add-host", 200, "POST", body); + await runTestResponse( + "/docker-config/hosts", + '[{"name":"test","url":"localhost:2375","secure":0},{"name":"Localhost","url":"localhost:2375","secure":0}]', + "GET", + ); + }); + + it("Check Host Updating", async () => { + const body: string = + '{"name":"test","url":"127.0.0.1:2375","secure":false}'; + + await runTestCode("/docker-config/update-host", 200, "POST", body); + await runTestResponse( + "/docker-config/hosts", + '[{"name":"test","url":"127.0.0.1:2375","secure":0},{"name":"Localhost","url":"localhost:2375","secure":0}]', + "GET", + ); + }); + + it("Check Config update", async () => { + const body = `{"fetching_interval":"1","keep_data_for":"1","api_key":${API_KEY}}`; + await runTestCode( + "/config/update", + 200, + "POST", + '{"fetching_interval":"1","keep_data_for":"1","api_key":"123"}', + ); + }); +}); From 20c29c4e73a14176511121b6a1a4c7bfd93a3a6b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 19:39:51 +0100 Subject: [PATCH 208/369] Feat: Unit tests and general adjustments --- .dockerignore | 3 + .github/workflows/docker.yaml | 31 +++-- out | 18 +++ package.json | 3 +- src/core/database/repository.ts | 117 +++++++++--------- src/core/docker/client.ts | 6 +- src/core/docker/monitor.ts | 10 +- src/core/docker/store-container-stats.ts | 24 ++-- .../procedures/docker-manager.procedure.ts | 16 +-- src/core/utils/helpers.ts | 15 +++ src/routes/docker-manager.ts | 26 ++-- src/routes/docker-stats.ts | 26 ++-- src/routes/docker-websocket.ts | 6 +- src/routes/utils.ts | 4 +- src/tests/cleanup.ts | 17 ++- src/tests/delete.spec.ts | 4 +- src/tests/gets.spec.ts | 6 +- src/tests/helper.ts | 98 +++++++-------- src/tests/post.spec.ts | 52 ++++---- src/typings/docker.ts | 5 +- 20 files changed, 280 insertions(+), 207 deletions(-) create mode 100644 out create mode 100644 src/core/utils/helpers.ts diff --git a/.dockerignore b/.dockerignore index db08b7e1..295585f1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,6 @@ *.dot *.mmd *.lock +src/tests +.github +.local-tests \ No newline at end of file diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 782a2dca..90e231be 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -15,24 +15,29 @@ jobs: name: Test Build on Push runs-on: ubuntu-latest steps: + - uses: oven-sh/setup-bun@v2 + name: Setup Bun + with: + bun-version: latest + - name: Checkout repository uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Run Unit-tests + run: | + bun install + bun clean + bun test - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Run Docker Build + run: | + bun build:docker - - name: Build image for testing - uses: docker/build-push-action@v6 - with: - context: . - file: docker/Dockerfile - load: true - # build only for current architecture (usually linux/amd64) to enable local loading - platforms: linux/amd64 - tags: dockstatapi:test + - name: Start docker container + run: | + docker run --name dockstatapi --rm -d dockstatapi:local + sleep 10 + if [[ $(docker container ls | grep "Up" | wc -l) -gt 0 ]]; then docker kill dockstatapi && exit 0; else; exit 1; fi release: name: Build and Push Docker Image on Release diff --git a/out b/out new file mode 100644 index 00000000..8ca3feff --- /dev/null +++ b/out @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/package.json b/package.json index 3964ec88..3a1dd228 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,12 @@ "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", "version": "3.0.0", "scripts": { - "test": "bun clean && bun test && bun run ./src/tests/cleanup.ts", "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", + "start:docker": "bun run build:docker && docker run -p 3001:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", "build": "bun build --target bun src/index.ts --outdir ./dist", + "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", "clean": "bun run clean:win || bun run clean:lin", "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q dockstatapi.db* && echo 'success'", "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f dockstatapi.db* && echo 'success'", diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 552b6a2b..96ab5336 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -31,8 +31,9 @@ export const dbFunctions = { ); CREATE TABLE IF NOT EXISTS docker_hosts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, - url TEXT NOT NULL, + hostadress TEXT NOT NULL, secure BOOLEAN NOT NULL ); @@ -87,20 +88,20 @@ export const dbFunctions = { const stmt = db.prepare( ` INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme") - `, + ` ); stmt.run(); } const hostRow = db - .prepare(`SELECT COUNT(*) AS count FROM docker_hosts WHERE name = ?`) + .prepare(`SELECT COUNT(*) AS count FROM docker_hosts`) .get("Localhost") as { count: number }; if (hostRow.count === 0) { logger.debug("Initializing default docker host (Localhost)"); const stmt = db.prepare( ` - INSERT INTO docker_hosts (name, url, secure) VALUES (?, ?, ?) - `, + INSERT INTO docker_hosts (name, hostadress, secure) VALUES (?, ?, ?) + ` ); stmt.run("Localhost", "localhost:2375", false); } @@ -109,40 +110,36 @@ export const dbFunctions = { logger.debug(`__task__ __db__ Initializing Database ✔️ (${duration}ms)`); }, - addDockerHost(hostId: string, url: string, secure: boolean) { + addDockerHost(host: DockerHost) { return executeDbOperation( "Add Docker Host", () => { const stmt = db.prepare(` - INSERT INTO docker_hosts (name, url, secure) + INSERT INTO docker_hosts (name, hostadress, secure) VALUES (?, ?, ?) `); - return stmt.run(hostId, url, secure); + return stmt.run(host.name, host.hostadress, host.secure); }, () => { - if (hostId.length < 1) { + if (host.name.length < 1) { logger.error("Hostname needed"); - throw new Error( - "Invalid data provided, please see server's log for more info", - ); + throw new Error("Invalid data provided - Hostname needed"); } - if (url.length < 1) { - logger.error("URL needed"); - throw new Error( - "Invalid data provided, please see server's log for more info", - ); + if (host.hostadress.length < 1) { + logger.error("Hostadress needed"); + throw new Error("Invalid data provided - Hostadress needed"); } if ( - typeof hostId !== "string" || - typeof url !== "string" || - typeof secure !== "boolean" + typeof host.name !== "string" || + typeof host.secure !== "boolean" || + typeof host.hostadress !== "string" ) { logger.error("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); } - }, + } ); }, @@ -151,13 +148,13 @@ export const dbFunctions = { "Get Docker Hosts", () => { const stmt = db.prepare(` - SELECT name, url, secure + SELECT id, name, hostadress, secure FROM docker_hosts - ORDER BY name DESC + ORDER BY id DESC `); return stmt.all() as DockerHost[]; }, - () => {}, + () => {} ); }, @@ -165,7 +162,7 @@ export const dbFunctions = { level: string, message: string, file_name: string, - line: number, + line: number ) => { if ( typeof level !== "string" || @@ -195,7 +192,7 @@ export const dbFunctions = { `); return stmt.all(); }, - () => {}, + () => {} ); }, @@ -216,50 +213,56 @@ export const dbFunctions = { logger.error("Level parameter must be a string"); throw new TypeError("Level parameter must be a string"); } - }, + } ); }, - updateDockerHost(name: string, url: string, secure: boolean) { + updateDockerHost(host: DockerHost) { return executeDbOperation( "Update Docker Host", () => { const stmt = db.prepare(` UPDATE docker_hosts - SET url = ?, secure = ? - WHERE name = ? + SET hostadress = ?, secure = ?, name = ? + WHERE id = ? `); - return stmt.run(url, secure, name); + return stmt.run( + host.hostadress, + host.secure, + host.name, + String(host.id) + ); }, () => { if ( - typeof name !== "string" || - typeof url !== "string" || - typeof secure !== "boolean" + typeof host.name !== "string" || + typeof host.hostadress !== "string" || + typeof host.secure !== "boolean" || + typeof host.id !== "number" ) { logger.error("Invalid parameter types for updateDockerHost"); throw new TypeError("Invalid parameter types for updateDockerHost"); } - }, + } ); }, - deleteDockerHost(name: string) { + deleteDockerHost(id: number) { return executeDbOperation( "Delete Docker Host", () => { const stmt = db.prepare(` DELETE FROM docker_hosts - WHERE name = ? + WHERE id = ? `); - return stmt.run(name); + return stmt.run(id); }, () => { - if (typeof name !== "string") { + if (typeof id !== "number") { logger.error("Invalid parameter type for deleteDockerHost"); throw new TypeError("Name parameter must be a string"); } - }, + } ); }, @@ -272,7 +275,7 @@ export const dbFunctions = { `); return stmt.run(); }, - () => {}, + () => {} ); }, @@ -291,14 +294,14 @@ export const dbFunctions = { logger.error("Invalid parameter type for clearLogsByLevel"); throw new TypeError("Level parameter must be a string"); } - }, + } ); }, updateConfig( fetching_interval: number, keep_data_for: number, - api_key: string, + api_key: string ) { return executeDbOperation( "Update Config", @@ -319,7 +322,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateConfig"); throw new TypeError("Invalid parameter types for updateConfig"); } - }, + } ); }, @@ -333,7 +336,7 @@ export const dbFunctions = { `); return stmt.all(); }, - () => {}, + () => {} ); }, @@ -358,7 +361,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteOldData"); throw new TypeError("Days parameter must be a number"); } - }, + } ); }, @@ -370,7 +373,7 @@ export const dbFunctions = { status: string, state: string, cpu_usage: number, - memory_usage: number, + memory_usage: number ) { return executeDbOperation( "Add Container Stats", @@ -387,7 +390,7 @@ export const dbFunctions = { status, state, cpu_usage, - memory_usage, + memory_usage ); }, () => { @@ -404,7 +407,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addContainerStats"); throw new TypeError("Invalid parameter types for addContainerStats"); } - }, + } ); }, @@ -457,10 +460,10 @@ export const dbFunctions = { stats.containersRunning, stats.containersStopped, stats.containersPaused, - stats.images, + stats.images ); }, - () => {}, + () => {} ); }, @@ -489,10 +492,10 @@ export const dbFunctions = { stack_config.container_count, stack_config.stack_prefix, stack_config.automatic_reboot_on_error, - stack_config.image_updates, + stack_config.image_updates ); }, - () => {}, + () => {} ); }, @@ -507,7 +510,7 @@ export const dbFunctions = { `); return stmt.all(); }, - () => {}, + () => {} ); }, @@ -521,7 +524,7 @@ export const dbFunctions = { `); return stmt.run(name); }, - () => {}, + () => {} ); }, @@ -549,10 +552,10 @@ export const dbFunctions = { stack_config.stack_prefix, stack_config.automatic_reboot_on_error, stack_config.image_updates, - stack_config.name, + stack_config.name ); }, - () => {}, + () => {} ); }, }; diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index 010a2bd6..324178b4 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -4,9 +4,9 @@ import { logger } from "~/core/utils/logger"; export const getDockerClient = (host: DockerHost): Docker => { try { - const inputUrl = host.url.includes("://") - ? host.url - : `${host.secure ? "https" : "http"}://${host.url}`; + const inputUrl = host.hostadress.includes("://") + ? host.hostadress + : `${host.secure ? "https" : "http"}://${host.hostadress}`; const parsedUrl = new URL(inputUrl); const hostAddress = parsedUrl.hostname; let port = parsedUrl.port diff --git a/src/core/docker/monitor.ts b/src/core/docker/monitor.ts index 4f56b2b0..1257326c 100644 --- a/src/core/docker/monitor.ts +++ b/src/core/docker/monitor.ts @@ -3,7 +3,7 @@ import { dbFunctions } from "~/core/database/repository"; import { getDockerClient } from "~/core/docker/client"; import { logger } from "~/core/utils/logger"; import { pluginManager } from "../plugins/plugin-manager"; -import { HostStats, ContainerInfo } from "~/typings/docker"; +import { ContainerInfo } from "~/typings/docker"; import { sleep } from "bun"; export async function monitorDockerEvents() { @@ -12,7 +12,7 @@ export async function monitorDockerEvents() { try { hosts = dbFunctions.getDockerHosts(); logger.debug( - `Retrieved ${hosts.length} Docker host(s) for event monitoring.`, + `Retrieved ${hosts.length} Docker host(s) for event monitoring.` ); } catch (error: unknown) { logger.error(`Error retrieving Docker hosts: ${(error as Error).message}`); @@ -58,7 +58,7 @@ async function startFor(host: DockerHost) { event = JSON.parse(line); } catch (parseErr: any) { logger.error( - `Failed to parse event from host ${host.name}: ${parseErr.message}`, + `Failed to parse event from host ${host.name}: ${parseErr.message}` ); continue; } @@ -113,7 +113,7 @@ async function startFor(host: DockerHost) { break; default: logger.debug( - `Unhandled container event "${action}" on host ${host.name}`, + `Unhandled container event "${action}" on host ${host.name}` ); } } @@ -132,7 +132,7 @@ async function startFor(host: DockerHost) { }); } catch (streamErr: any) { logger.error( - `Failed to start events stream for host ${host.name}: ${streamErr.message}`, + `Failed to start events stream for host ${host.name}: ${streamErr.message}` ); } } diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts index e64f31bc..6bcc1684 100644 --- a/src/core/docker/store-container-stats.ts +++ b/src/core/docker/store-container-stats.ts @@ -5,10 +5,12 @@ import { calculateCpuPercent, calculateMemoryUsage, } from "~/core/utils/calculations"; +import { logger } from "../utils/logger"; async function storeContainerData() { try { const hosts = dbFunctions.getDockerHosts(); + logger.debug("Retrieved docker hosts for storring container data"); // Process each host concurrently and wait for them all to finish await Promise.all( @@ -21,7 +23,7 @@ async function storeContainerData() { } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); throw new Error( - `Failed to ping docker host "${host.name}": ${errMsg}`, + `Failed to ping docker host "${host.name}": ${errMsg}` ); } @@ -31,7 +33,7 @@ async function storeContainerData() { } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); throw new Error( - `Failed to list containers on host "${host.name}": ${errMsg}`, + `Failed to list containers on host "${host.name}": ${errMsg}` ); } @@ -50,20 +52,20 @@ async function storeContainerData() { error instanceof Error ? error.message : String(error); return reject( new Error( - `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, - ), + `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}` + ) ); } if (!stats) { return reject( new Error( - `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".`, - ), + `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".` + ) ); } resolve(stats); }); - }, + } ); dbFunctions.addContainerStats( @@ -74,18 +76,18 @@ async function storeContainerData() { containerInfo.Status, containerInfo.State, calculateCpuPercent(stats), - calculateMemoryUsage(stats), + calculateMemoryUsage(stats) ); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); throw new Error( - `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, + `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}` ); } - }), + }) ); - }), + }) ); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); diff --git a/src/core/trpc/procedures/docker-manager.procedure.ts b/src/core/trpc/procedures/docker-manager.procedure.ts index 958b31b8..93b6a973 100644 --- a/src/core/trpc/procedures/docker-manager.procedure.ts +++ b/src/core/trpc/procedures/docker-manager.procedure.ts @@ -3,26 +3,26 @@ import { logger } from "~/core/utils/logger"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { router, publicProcedure } from "../trpc"; +import { DockerHost } from "~/typings/docker"; const addHostInput = z.object({ name: z.string(), - url: z.string(), + hostadress: z.string(), secure: z.boolean(), }); const updateHostInput = z.object({ name: z.string(), - url: z.string(), + hostadress: z.string(), secure: z.boolean(), }); export const dockerManagerProcedure = router({ addHost: publicProcedure.input(addHostInput).mutation(({ input }) => { try { - const { name, url, secure } = input; - dbFunctions.addDockerHost(name, url, secure); - logger.debug(`Added docker host (${name})`); - return { success: true, message: `Added docker host (${name})` }; + dbFunctions.addDockerHost(input as DockerHost); + logger.debug(`Added docker host (${input.name})`); + return { success: true, message: `Added docker host (${input.name})` }; } catch (error) { logger.error("Error adding docker host", error); throw new TRPCError({ @@ -35,8 +35,8 @@ export const dockerManagerProcedure = router({ updateHost: publicProcedure.input(updateHostInput).mutation(({ input }) => { try { - const { name, url, secure } = input; - dbFunctions.updateDockerHost(name, url, secure); + (input as unknown as DockerHost).id = "0"; + dbFunctions.updateDockerHost(input as DockerHost); return { success: true, message: `Updated docker host (${name})` }; } catch (error) { logger.error("Error updating docker host", error); diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts new file mode 100644 index 00000000..1bc02063 --- /dev/null +++ b/src/core/utils/helpers.ts @@ -0,0 +1,15 @@ +import { logger } from "./logger"; + +export function findObjectByKey( + array: T[], + key: keyof T, + value: T[keyof T] +): T | undefined { + const data = array.find((item) => item[key] === value); + logger.debug( + `Searching ${String(key)} = ${String(value)} in ${String( + JSON.stringify(array) + )} Found Item ${JSON.stringify(data)}` + ); + return data; +} diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 1d351013..6db7362e 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -8,15 +8,14 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) "/add-host", async ({ set, body }) => { try { - const { name, url, secure } = body; set.headers["Content-Type"] = "application/json"; - dbFunctions.addDockerHost(name, url, secure); - return responseHandler.ok(set, `Added docker host (${name})`); + dbFunctions.addDockerHost(body); + return responseHandler.ok(set, `Added docker host (${body.name})`); } catch (error: unknown) { return responseHandler.error( set, "Error adding docker Host", - error as string, + error as string ); } }, @@ -27,23 +26,23 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) }, body: t.Object({ name: t.String(), - url: t.String(), + hostadress: t.String(), secure: t.Boolean(), }), - }, + } ) .post( "/update-host", async ({ set, body }) => { try { - const { name, url, secure } = body; - dbFunctions.updateDockerHost(name, url, secure); + set.status = 200; + return dbFunctions.updateDockerHost(body); } catch (error) { return responseHandler.error( set, error as string, - "Failed to update host", + "Failed to update host" ); } }, @@ -53,11 +52,12 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) description: "Update an already existing target's config", }, body: t.Object({ + id: t.Number(), name: t.String(), - url: t.String(), + hostadress: t.String(), secure: t.Boolean(), }), - }, + } ) .get( @@ -72,7 +72,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) return responseHandler.error( set, error as string, - "Failed to retrieve hosts", + "Failed to retrieve hosts" ); } }, @@ -81,5 +81,5 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) tags: ["Management"], description: "Returns an Array of Host-config-objects", }, - }, + } ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 9eb34036..4324fef5 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -29,7 +29,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, pingError as string, - "Docker host connection failed", + "Docker host connection failed" ); } @@ -47,24 +47,24 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) set, reject, "An error occurred", - error, + error ); } if (!stats) { return responseHandler.reject( set, reject, - "No stats available", + "No stats available" ); } resolve(stats); }); - }, + } ); containers.push({ id: containerInfo.Id, - hostId: host.name, + hostId: host.id as string, name: containerInfo.Names[0].replace(/^\//, ""), image: containerInfo.Image, status: containerInfo.Status, @@ -75,16 +75,16 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) } catch (containerError) { logger.error( "Error fetching container stats,", - containerError, + containerError ); } - }), + }) ); logger.debug(`Fetched stats for ${host.name}`); } catch (hostError) { logger.error("Error fetching containers for host,", hostError); } - }), + }) ); set.headers["Content-Type"] = "application/json"; @@ -94,7 +94,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, error as string, - "Failed to retrieve containers", + "Failed to retrieve containers" ); } }, @@ -104,7 +104,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) description: "Fetches all Containers and their statistics across all Hosts", }, - }, + } ) .get( @@ -117,7 +117,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) if (!host) { return responseHandler.simple_error( set, - `Host (${params.id}) not found`, + `Host (${params.id}) not found` ); } @@ -147,7 +147,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, error as string, - "Failed to retrieve host config", + "Failed to retrieve host config" ); } }, @@ -156,5 +156,5 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) tags: ["Statistics"], description: "Fetches the Host Stats for a specified Host", }, - }, + } ); diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 5520579c..bd9571b7 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -39,7 +39,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( const docker = getDockerClient(host); await docker.ping(); const containers = await docker.listContainers(); - logger.debug(`Found ${containers.length} containers on ${host.name}`); + logger.debug( + `Found ${containers.length} containers on ${host.name} (id: ${host.id})` + ); for (const containerInfo of containers) { if (ws.readyState !== 1) { @@ -64,7 +66,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( ws.send( JSON.stringify({ id: containerInfo.Id, - hostId: host.name, + hostId: host.id as string, name: containerInfo.Names[0].replace(/^\//, ""), image: containerInfo.Image, status: containerInfo.Status, diff --git a/src/routes/utils.ts b/src/routes/utils.ts index 3afbf35b..22534244 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -32,7 +32,7 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( return responseHandler.error( set, error.message || error, - "Error getting DockStatAPI information", + "Error getting DockStatAPI information" ); } }, @@ -41,5 +41,5 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( tags: ["Utils"], description: "Shows general information about DockStatAPI", }, - }, + } ); diff --git a/src/tests/cleanup.ts b/src/tests/cleanup.ts index 6965dd40..163419ae 100644 --- a/src/tests/cleanup.ts +++ b/src/tests/cleanup.ts @@ -1,7 +1,20 @@ import { dbFunctions } from "~/core/database/repository"; +import type { DockerHost } from "~/typings/docker"; +import { findObjectByKey } from "~/core/utils/helpers"; console.log(""); console.log("Deleting `test` Docker host"); -dbFunctions.deleteDockerHost("test"); -console.log("Cleanuing up Database config to default values"); + +let testHosts: DockerHost[] = dbFunctions.getDockerHosts(); + +const testHost = findObjectByKey(testHosts, "name", "test"); + +if (testHost) { + dbFunctions.deleteDockerHost(testHost.id as number); + console.log(`Docker host with name "${testHost.name}" deleted.`); +} else { + console.log("Docker host not found."); +} + +console.log("Cleaning up Database config to default values"); dbFunctions.updateConfig(5, 7, "changeme"); diff --git a/src/tests/delete.spec.ts b/src/tests/delete.spec.ts index ee8ad22a..37e36fc9 100644 --- a/src/tests/delete.spec.ts +++ b/src/tests/delete.spec.ts @@ -3,10 +3,10 @@ import { runTestCode } from "./helper"; describe("DockStatAPI (DELETE)", () => { it("Delete all Logs /logs", async () => { - await runTestCode("/logs", 200, "DELETE", "{}"); + await runTestCode("/logs", 200, "DELETE", {}); }); it("Delete Logs (Debug) /logs/debug", async () => { - await runTestCode("/logs/debug", 200, "DELETE", "{}"); + await runTestCode("/logs/debug", 200, "DELETE", {}); }); }); diff --git a/src/tests/gets.spec.ts b/src/tests/gets.spec.ts index f211c39f..27235083 100644 --- a/src/tests/gets.spec.ts +++ b/src/tests/gets.spec.ts @@ -42,7 +42,7 @@ describe("DockStatAPI (GET)", () => { }); it("Check /config/package", async () => { - const expected = { + const expected = JSON.stringify({ version, description, license, @@ -52,8 +52,8 @@ describe("DockStatAPI (GET)", () => { contributors, dependencies, devDependencies, - }; + }); - await runTestResponse("/config/package", JSON.stringify(expected), "GET"); + await runTestResponse("/config/package", expected, "GET"); }); }); diff --git a/src/tests/helper.ts b/src/tests/helper.ts index 1c773df9..bd03055f 100644 --- a/src/tests/helper.ts +++ b/src/tests/helper.ts @@ -1,57 +1,62 @@ import { expect } from "bun:test"; import { DockStatAPI } from ".."; import { logger } from "~/core/utils/logger"; + export const API_KEY = "TestKey"; +const server = "http://localhost:3001"; export async function runTestResponse( path: string, - expected_response: string, + expected_response: any, method?: "GET" | "POST" | "DELETE", + requestBody?: any ) { - if (!method) { - method = "GET"; - } - - const server = "http://localhost:3000"; + method = method || "GET"; const route = `${server}${path}`; - logger.info(`__UT__ [START] Running test, method: ${method} on ${route}`); + logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); const startTime = Date.now(); try { + const processedBody = + requestBody !== undefined + ? typeof requestBody === "string" + ? requestBody + : JSON.stringify(requestBody) + : undefined; + const request = new Request(route, { method, - verbose: true, + body: processedBody, headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, }); + logger.debug( - `__UT__ Request details: ${JSON.stringify({ + `Request details: ${JSON.stringify({ url: route, method, headers: [...request.headers], - })}`, + body: processedBody, + })}` ); - // Get the response const response = await DockStatAPI.handle(request); - const headers: any = {}; - response.headers.forEach((value, key: any) => { - headers[key] = value; - }); - logger.debug(`__UT__ Received HTTP status: ${response.status}`); - logger.debug(`__UT__ Response headers: ${JSON.stringify(headers)}`); + const headers: { [key: string]: string } = {}; + response.headers.forEach((value, key) => (headers[key] = value)); - // Log the response body as text const responseText = await response.text(); const duration = Date.now() - startTime; - logger.debug(`__UT__ Response body: ${responseText}`); - logger.debug(`__UT__ Total Duration: ${duration}ms`); - logger.info(`__UT__ [END] Completed test on ${route}`); - return expect(responseText).toBe(expected_response); + logger.debug(`Received HTTP status: ${response.status}`); + logger.debug(`Response headers: ${JSON.stringify(headers)}`); + logger.debug(`Response body: ${responseText}`); + logger.debug(`Total Duration: ${duration}ms`); + + expect(responseText).toBe(expected_response); + logger.info(`__UT__ [ END ] Completed test on ${route}`); } catch (error) { logger.error(`__UT__ Error during test on ${route}: ${error}`); throw error; @@ -62,56 +67,51 @@ export async function runTestCode( path: string, expected_code: number, method?: "GET" | "POST" | "DELETE", - requestBody?: string, + requestBody?: any ) { - if (!method) { - method = "GET"; - } - - if (!requestBody) { - requestBody = ""; - } - - const server = "http://localhost:3000"; + method = method || "GET"; const route = `${server}${path}`; - logger.info(`__UT__ [START] Running test, method: ${method} on ${route}`); + logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); const startTime = Date.now(); try { + const processedBody = + requestBody !== undefined + ? typeof requestBody === "string" + ? requestBody + : JSON.stringify(requestBody) + : undefined; + const request = new Request(route, { method, - verbose: true, - body: requestBody, + body: processedBody, headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, }); + logger.debug( - `__UT__ Request details: ${JSON.stringify({ + `Request details: ${JSON.stringify({ url: route, method, headers: [...request.headers], - body: requestBody, - })}`, + body: processedBody, + })}` ); const response = await DockStatAPI.handle(request); - logger.debug(`__UT__ Received HTTP status: ${response.status}`); - - const headers: any = {}; - response.headers.forEach((value, key) => { - headers[key] = value; - }); - - logger.debug(`__UT__ Response headers: ${JSON.stringify(headers)}`); - logger.debug(`__UT__ Response: ${JSON.stringify(response.body)}`); - + const headers: { [key: string]: string } = {}; + response.headers.forEach((value, key) => (headers[key] = value)); const duration = Date.now() - startTime; - logger.debug(`__UT__ Completed test on ${route} (Duration: ${duration}ms)`); + + logger.debug(`Received HTTP status: ${response.status}`); + logger.debug(`Response headers: ${JSON.stringify(headers)}`); + logger.debug(`Response body: ${JSON.stringify(response.body)}`); expect(response.status).toBe(expected_code); + logger.debug(`__UT__ Completed test on ${route} (Duration: ${duration}ms)`); } catch (error) { logger.error(`__UT__ Error during test on ${route}: ${error}`); throw error; diff --git a/src/tests/post.spec.ts b/src/tests/post.spec.ts index 4d780eee..f581770d 100644 --- a/src/tests/post.spec.ts +++ b/src/tests/post.spec.ts @@ -1,40 +1,50 @@ import { describe, it } from "bun:test"; import { runTestResponse, runTestCode } from "./helper"; -import { dbFunctions } from "~/core/database/repository"; -import { API_KEY } from "./helper"; +import { DockerHost } from "~/typings/docker"; describe("DockStatAPI (POST)", () => { it("Check Host adding", async () => { - const body: string = - '{"name":"test","url":"localhost:2375","secure":false}'; + const body = { + name: "test", + hostadress: "localhost:2375", + secure: false, + }; await runTestCode("/docker-config/add-host", 200, "POST", body); - await runTestResponse( - "/docker-config/hosts", - '[{"name":"test","url":"localhost:2375","secure":0},{"name":"Localhost","url":"localhost:2375","secure":0}]', - "GET", - ); + await runTestCode("/docker-config/hosts", 200, "GET"); }); it("Check Host Updating", async () => { - const body: string = - '{"name":"test","url":"127.0.0.1:2375","secure":false}'; + const codeBody: DockerHost = { + id: 2, + name: "test", + hostadress: "127.0.0.1:2375", + secure: false, + }; - await runTestCode("/docker-config/update-host", 200, "POST", body); + await runTestCode("/docker-config/update-host", 200, "POST", codeBody); + + const responseBody: DockerHost[] = [ + { id: 2, name: "test", hostadress: "127.0.0.1:2375", secure: 0 }, + { + id: 1, + name: "Localhost", + hostadress: "localhost:2375", + secure: 0, + }, + ]; await runTestResponse( "/docker-config/hosts", - '[{"name":"test","url":"127.0.0.1:2375","secure":0},{"name":"Localhost","url":"localhost:2375","secure":0}]', - "GET", + JSON.stringify(responseBody), + "GET" ); }); it("Check Config update", async () => { - const body = `{"fetching_interval":"1","keep_data_for":"1","api_key":${API_KEY}}`; - await runTestCode( - "/config/update", - 200, - "POST", - '{"fetching_interval":"1","keep_data_for":"1","api_key":"123"}', - ); + await runTestCode("/config/update", 200, "POST", { + fetching_interval: 1, + keep_data_for: 1, + api_key: "TestKey", + }); }); }); diff --git a/src/typings/docker.ts b/src/typings/docker.ts index 522762c2..f295ab39 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -1,7 +1,8 @@ interface DockerHost { name: string; - url: string; - secure: boolean; + hostadress: string; + secure: boolean | number; + id?: number; } interface ContainerInfo { From 2aae57c7fc04503c0da6326ad9374742957f91ae Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 21 Mar 2025 18:40:21 +0000 Subject: [PATCH 209/369] Update dependency graphs --- dependency-graph.dot | 2 + dependency-graph.mmd | 2 + dependency-graph.svg | 1044 +++++++++++++++++++++--------------------- 3 files changed, 532 insertions(+), 516 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index f6d55f95..48a4a931 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -36,6 +36,7 @@ strict digraph "dependency-cruiser output"{ "src/core/docker/scheduler.ts" -> "src/core/utils/logger.ts" "src/core/docker/scheduler.ts" -> "src/typings/database.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-container-stats.ts" [label= tooltip="store-container-stats.ts" URL="src/core/docker/store-container-stats.ts" fillcolor="#ddfeff"] } } } + "src/core/docker/store-container-stats.ts" -> "src/core/utils/logger.ts" "src/core/docker/store-container-stats.ts" -> "src/core/database/repository.ts" "src/core/docker/store-container-stats.ts" -> "src/core/docker/client.ts" "src/core/docker/store-container-stats.ts" -> "src/core/utils/calculations.ts" @@ -73,6 +74,7 @@ strict digraph "dependency-cruiser output"{ "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/trpc/trpc.ts" "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/database/repository.ts" "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/utils/logger.ts" + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/typings/docker.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-stats.procedure.ts" [label= tooltip="docker-stats.procedure.ts" URL="src/core/trpc/procedures/docker-stats.procedure.ts" fillcolor="#ddfeff"] } } } } "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/trpc/trpc.ts" "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/database/repository.ts" diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 01974834..b845e1e8 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -149,6 +149,7 @@ Y-->Z 11-->A 11-->I 11-->12 +13-->A 13-->D 13-->O 13-->14 @@ -174,6 +175,7 @@ Y-->Z 1F-->1E 1F-->D 1F-->A +1F-->I 1G-->1E 1G-->D 1G-->O diff --git a/dependency-graph.svg b/dependency-graph.svg index 89342e89..412e5586 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,82 +4,82 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/trpc - -trpc + +trpc cluster_src/core/trpc/procedures - -procedures + +procedures cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -87,8 +87,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -96,8 +96,8 @@ events - -events + +events @@ -105,8 +105,8 @@ fs - -fs + +fs @@ -114,8 +114,8 @@ fs/promises - -promises + +promises @@ -123,8 +123,8 @@ package.json - -package.json + +package.json @@ -132,8 +132,8 @@ path - -path + +path @@ -141,8 +141,8 @@ src/core/database/helper.ts - -helper.ts + +helper.ts @@ -150,471 +150,477 @@ src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + src/core/database/repository.ts - -repository.ts + +repository.ts - + src/core/utils/logger.ts->src/core/database/repository.ts - - - - + + + + src/routes/live-logs.ts - -live-logs.ts + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/typings/websocket.ts - -websocket.ts + +websocket.ts - + src/core/utils/logger.ts->src/typings/websocket.ts - - + + src/core/database/repository.ts->bun:sqlite - - + + src/core/database/repository.ts->src/core/database/helper.ts - - - - + + + + src/core/database/repository.ts->src/core/utils/logger.ts - - - - + + + + src/typings/database.ts - -database.ts + +database.ts src/core/database/repository.ts->src/typings/database.ts - - + + src/typings/docker.ts - -docker.ts + +docker.ts src/core/database/repository.ts->src/typings/docker.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->bun - - + + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->src/core/database/repository.ts - - + + src/core/docker/monitor.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + src/typings/plugin.ts - -plugin.ts + +plugin.ts - + src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/repository.ts - - + + src/core/docker/scheduler.ts->src/typings/database.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - + + - + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + src/typings/dockerode.ts - -dockerode.ts + +dockerode.ts - + src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + - + +src/core/docker/store-container-stats.ts->src/core/utils/logger.ts + + + + + src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/repository.ts - - + + - + src/core/stacks/controller.ts->src/typings/database.ts - - + + src/typings/docker-compose.ts - -docker-compose.ts + +docker-compose.ts - + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + src/core/trpc/index.ts - -index.ts + +index.ts @@ -622,267 +628,273 @@ src/core/trpc/router.ts - -router.ts + +router.ts - + src/core/trpc/index.ts->src/core/trpc/router.ts - - + + src/core/trpc/procedures/api-config.procedure.ts - -api-config.procedure.ts + +api-config.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - + + src/core/trpc/trpc.ts - -trpc.ts + +trpc.ts - + src/core/trpc/router.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts - -docker-manager.procedure.ts + +docker-manager.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts - -docker-stats.procedure.ts + +docker-stats.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - + + src/core/trpc/procedures/logs.procedure.ts - -logs.procedure.ts + +logs.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - + + src/core/trpc/procedures/stacks.procedure.ts - -stacks.procedure.ts + +stacks.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - + + - + src/core/utils/package-json.ts->package.json - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/typings/docker.ts + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->src/typings/websocket.ts - - + + @@ -894,427 +906,427 @@ - + src/typings/websocket.ts->stream - - + + src/core/utils/respone-handler.ts - -respone-handler.ts + +respone-handler.ts - + src/core/utils/respone-handler.ts->src/core/utils/logger.ts - - + + src/typings/elysiajs.ts - -elysiajs.ts + +elysiajs.ts - + src/core/utils/respone-handler.ts->src/typings/elysiajs.ts - - + + src/index.ts - -index.ts + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/repository.ts - - + + - + src/index.ts->src/typings/database.ts - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/core/trpc/index.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + src/middleware/auth.ts - -auth.ts + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + src/routes/stacks.ts - -stacks.ts + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + src/routes/utils.ts - -utils.ts + +utils.ts - + src/index.ts->src/routes/utils.ts - - + + src/routes/api-config.ts - -api-config.ts + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + src/routes/docker-manager.ts - -docker-manager.ts + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + src/routes/docker-stats.ts - -docker-stats.ts + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + src/routes/docker-websocket.ts - -docker-websocket.ts + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + src/routes/logs.ts - -logs.ts + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/repository.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/repository.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/utils.ts->src/core/utils/package-json.ts - - + + - + src/routes/utils.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/repository.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->src/core/database/repository.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->src/core/database/repository.ts - - + + - + src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/repository.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts - - + + - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/repository.ts - - + + From 6131d8a45bb5d510e7b12b2bf12e6e6a03f9f3b9 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 19:42:00 +0100 Subject: [PATCH 210/369] Fix: Add dependant docker socket prroxy --- .github/workflows/docker.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 90e231be..b3145bd2 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -23,6 +23,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Start proxy + run: | + docker compose -f docker/docker-compose.dev.yaml up -d + - name: Run Unit-tests run: | bun install From c63e0ebd02ddf40d76a559a8614fc6f5ae32c0b2 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 19:43:42 +0100 Subject: [PATCH 211/369] Fix: Name have to be written lowercase? --- .github/workflows/docker.yaml | 2 ++ docker/docker-compose.dev.yaml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index b3145bd2..e5d57791 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -25,6 +25,8 @@ jobs: - name: Start proxy run: | + pwd + ls -lah docker compose -f docker/docker-compose.dev.yaml up -d - name: Run Unit-tests diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index 39da6d6b..5dc2c338 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -1,7 +1,7 @@ name: "DockStatAPI - Dev" services: socket-proxy: - container_name: Socket-Proxy + container_name: socket-proxy image: lscr.io/linuxserver/socket-proxy:latest volumes: - /var/run/docker.sock:/var/run/docker.sock:ro @@ -42,7 +42,7 @@ services: - VOLUMES=1 #optional sqlite-web: - container_name: SQLite-web + container_name: qlite-web image: ghcr.io/coleifer/sqlite-web:latest ports: - 8080:8080 From d3178ae6469a3e5388158a40709dcdcdd33270b5 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 19:45:29 +0100 Subject: [PATCH 212/369] Fixx: That name? --- .dockerignore | 1 + docker/docker-compose.dev.yaml | 2 +- out | 18 ------------------ 3 files changed, 2 insertions(+), 19 deletions(-) delete mode 100644 out diff --git a/.dockerignore b/.dockerignore index 295585f1..e6dbc5af 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,6 +4,7 @@ *.md /docker *.dot +*.svg *.mmd *.lock src/tests diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index 5dc2c338..cca8f34a 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -1,4 +1,4 @@ -name: "DockStatAPI - Dev" +name: "dockstatapi-dev" services: socket-proxy: container_name: socket-proxy diff --git a/out b/out deleted file mode 100644 index 8ca3feff..00000000 --- a/out +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - From b79ca8e59f654189ae4ab726bf4ac6e14e7087fc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 19:48:17 +0100 Subject: [PATCH 213/369] Fix: Adjustments to checking for container start --- .github/workflows/docker.yaml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index e5d57791..b917f7f7 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -39,11 +39,16 @@ jobs: run: | bun build:docker - - name: Start docker container + - name: Start Docker container and check uptime run: | docker run --name dockstatapi --rm -d dockstatapi:local - sleep 10 - if [[ $(docker container ls | grep "Up" | wc -l) -gt 0 ]]; then docker kill dockstatapi && exit 0; else; exit 1; fi + sleep 30 + if docker ps --filter "name=dockstatapi" --filter "status=running" | grep dockstatapi; then + docker kill dockstatapi + exit 0 + else + exit 1 + fi release: name: Build and Push Docker Image on Release From 8579a13959e01f0ec3ca6fa25da1a79181a9e26e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 19:57:52 +0100 Subject: [PATCH 214/369] Fix: Docs update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 87fea6f1..fad06994 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Docker monitoring API with real-time statistics, stack management, and plugin su - **Database**: SQLite (WAL mode) - **Docker**: dockerode + compose - **Monitoring**: Custom metrics collection -- **Auth**: (TODO - Currently open) +- **Auth**: [Authentication](https://outline.itsnik.de/s/dockstat/doc/authentication-VSGhxqjtXf) ## Documentation and Wiki @@ -36,4 +36,4 @@ Please see [DockStatAPI](https://dockstatapi.itsnik.de) ![Dependency Graph](./dependency-graph.svg) -Click [here](./dependency-graph.mmd) for the mermaid version +Click [here](./dependency-graph.mmd) for the mermaid version. From 4828551a7c23d7c5ddfee39c757ebbf3c2b89b96 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 20:11:56 +0100 Subject: [PATCH 215/369] Fixxx: Mismatch in error message for deleteDockerHost parameter. Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/core/database/repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 96ab5336..34c36193 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -260,7 +260,7 @@ export const dbFunctions = { () => { if (typeof id !== "number") { logger.error("Invalid parameter type for deleteDockerHost"); - throw new TypeError("Name parameter must be a string"); + throw new TypeError("id parameter must be a number"); } } ); From 992095263ee6369b6a3de3c38d5d1e2ddba58192 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 20:12:22 +0100 Subject: [PATCH 216/369] Fix: Falsy check for a boolean flag may lead to false positives. Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/routes/stacks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index c4e6c2cb..ad961946 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -25,7 +25,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) if (!body.compose_spec) { missingParams.push("compose_spec"); } - if (!body.automatic_reboot_on_error) { + if (body.automatic_reboot_on_error === undefined) { missingParams.push("automatic_reboot_on_error"); } if (!body.source) { From 69faa814a2526eac7801a91d97b900155a40e9c9 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 20:12:43 +0100 Subject: [PATCH 217/369] Fix: Unreachable code in logger format function. Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/core/utils/logger.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 903df1f9..fce5909a 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -132,10 +132,6 @@ export const logger = createLogger({ } return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; - - const fullMessage = `${coloredLevel} [ ${coloredTimestamp} ] - ${message} - [ ${coloredContext} ]`; - - return formatTerminalMessage(fullMessage, prefixLength); }), ), transports: [new transports.Console()], From aaa8c73da01d1fea462b8060c9af574e8da8ce94 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 20:14:03 +0100 Subject: [PATCH 218/369] Fix: Typo Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fad06994..36384590 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Docker monitoring API with real-time statistics, stack management, and plugin su ## Tech Stack -- **Runtime**: [Bun.sh](http://Bun.sh) +- **Runtime**: [Bun.sh](https://bun.sh) - **Framework**: [Elysia.js](https://elysiajs.com/) - **Database**: SQLite (WAL mode) - **Docker**: dockerode + compose From 8c9f59a30150a99a0d89c11ada95a1f83cc0e306 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 20:16:33 +0100 Subject: [PATCH 219/369] Fix: See https://github.com/Its4Nik/DockStatAPI/pull/43#pullrequestreview-2706962510 --- src/core/database/repository.ts | 24 +++++----- src/core/docker/client.ts | 6 +-- .../procedures/docker-manager.procedure.ts | 8 ++-- src/core/utils/calculations.ts | 9 ++++ ...respone-handler.ts => response-handler.ts} | 0 src/middleware/auth.ts | 3 +- src/routes/api-config.ts | 20 ++++----- src/routes/docker-manager.ts | 6 +-- src/routes/docker-stats.ts | 2 +- src/routes/docker-websocket.ts | 8 ++-- src/routes/stacks.ts | 44 +++++++++---------- src/routes/utils.ts | 2 +- src/tests/post.spec.ts | 8 ++-- src/typings/docker.ts | 2 +- 14 files changed, 76 insertions(+), 66 deletions(-) rename src/core/utils/{respone-handler.ts => response-handler.ts} (100%) diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index 96ab5336..680affe7 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -33,7 +33,7 @@ export const dbFunctions = { CREATE TABLE IF NOT EXISTS docker_hosts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, - hostadress TEXT NOT NULL, + hostAddress TEXT NOT NULL, secure BOOLEAN NOT NULL ); @@ -100,7 +100,7 @@ export const dbFunctions = { logger.debug("Initializing default docker host (Localhost)"); const stmt = db.prepare( ` - INSERT INTO docker_hosts (name, hostadress, secure) VALUES (?, ?, ?) + INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?) ` ); stmt.run("Localhost", "localhost:2375", false); @@ -115,10 +115,10 @@ export const dbFunctions = { "Add Docker Host", () => { const stmt = db.prepare(` - INSERT INTO docker_hosts (name, hostadress, secure) + INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?) `); - return stmt.run(host.name, host.hostadress, host.secure); + return stmt.run(host.name, host.hostAddress, host.secure); }, () => { if (host.name.length < 1) { @@ -126,15 +126,15 @@ export const dbFunctions = { throw new Error("Invalid data provided - Hostname needed"); } - if (host.hostadress.length < 1) { - logger.error("Hostadress needed"); - throw new Error("Invalid data provided - Hostadress needed"); + if (host.hostAddress.length < 1) { + logger.error("hostAddress needed"); + throw new Error("Invalid data provided - hostAddress needed"); } if ( typeof host.name !== "string" || typeof host.secure !== "boolean" || - typeof host.hostadress !== "string" + typeof host.hostAddress !== "string" ) { logger.error("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); @@ -148,7 +148,7 @@ export const dbFunctions = { "Get Docker Hosts", () => { const stmt = db.prepare(` - SELECT id, name, hostadress, secure + SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC `); @@ -223,11 +223,11 @@ export const dbFunctions = { () => { const stmt = db.prepare(` UPDATE docker_hosts - SET hostadress = ?, secure = ?, name = ? + SET hostAddress = ?, secure = ?, name = ? WHERE id = ? `); return stmt.run( - host.hostadress, + host.hostAddress, host.secure, host.name, String(host.id) @@ -236,7 +236,7 @@ export const dbFunctions = { () => { if ( typeof host.name !== "string" || - typeof host.hostadress !== "string" || + typeof host.hostAddress !== "string" || typeof host.secure !== "boolean" || typeof host.id !== "number" ) { diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index 324178b4..7d8e6ea4 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -4,9 +4,9 @@ import { logger } from "~/core/utils/logger"; export const getDockerClient = (host: DockerHost): Docker => { try { - const inputUrl = host.hostadress.includes("://") - ? host.hostadress - : `${host.secure ? "https" : "http"}://${host.hostadress}`; + const inputUrl = host.hostAddress.includes("://") + ? host.hostAddress + : `${host.secure ? "https" : "http"}://${host.hostAddress}`; const parsedUrl = new URL(inputUrl); const hostAddress = parsedUrl.hostname; let port = parsedUrl.port diff --git a/src/core/trpc/procedures/docker-manager.procedure.ts b/src/core/trpc/procedures/docker-manager.procedure.ts index 93b6a973..7621a5a4 100644 --- a/src/core/trpc/procedures/docker-manager.procedure.ts +++ b/src/core/trpc/procedures/docker-manager.procedure.ts @@ -7,14 +7,15 @@ import { DockerHost } from "~/typings/docker"; const addHostInput = z.object({ name: z.string(), - hostadress: z.string(), + hostAddress: z.string(), secure: z.boolean(), }); const updateHostInput = z.object({ name: z.string(), - hostadress: z.string(), + hostAddress: z.string(), secure: z.boolean(), + id: z.number(), }); export const dockerManagerProcedure = router({ @@ -35,8 +36,7 @@ export const dockerManagerProcedure = router({ updateHost: publicProcedure.input(updateHostInput).mutation(({ input }) => { try { - (input as unknown as DockerHost).id = "0"; - dbFunctions.updateDockerHost(input as DockerHost); + dbFunctions.updateDockerHost(input); return { success: true, message: `Updated docker host (${name})` }; } catch (error) { logger.error("Error updating docker host", error); diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index 3d7a81d9..9428bf98 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -10,6 +10,15 @@ const calculateCpuPercent = (stats: Docker.ContainerStats): number => { stats.precpu_stats.cpu_usage.total_usage; const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + + if (cpuDelta <= 0) { + return 0; + } + + if (systemDelta <= 0) { + return 0; + } + return (cpuDelta / systemDelta) * 100; }; diff --git a/src/core/utils/respone-handler.ts b/src/core/utils/response-handler.ts similarity index 100% rename from src/core/utils/respone-handler.ts rename to src/core/utils/response-handler.ts diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 5ec3f19a..2672f88a 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -72,8 +72,7 @@ export async function validateApiKey(request: Request, set: set) { return { error: "Invalid API key" }; } - logger.info(`Valid API key used: ${apiKey}`); - return { apiKey }; + return logger.info(`Valid API key used`); } catch (error) { logger.error("Error during API key validation", error); set.status = 500; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index a74f34b7..17ea3521 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -1,7 +1,7 @@ import { Elysia, t } from "elysia"; import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/respone-handler"; +import { responseHandler } from "~/core/utils/response-handler"; import { config } from "~/typings/database"; import { version, @@ -32,7 +32,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error getting the DockStatAPI config", + "Error getting the DockStatAPI config" ); } }, @@ -41,7 +41,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Returns DockStatAPI's config", }, - }, + } ) .get( "/plugins", @@ -52,11 +52,11 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error getting all registered plugins", + "Error getting all registered plugins" ); } }, - { detail: { tags: ["Management"], description: "List all Plugin Names" } }, + { detail: { tags: ["Management"], description: "List all Plugin Names" } } ) .post( "/update", @@ -67,14 +67,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) dbFunctions.updateConfig( fetching_interval, keep_data_for, - await hashApiKey(api_key), + await hashApiKey(api_key) ); return responseHandler.ok(set, "Updated DockStatAPI config"); } catch (error) { return responseHandler.error( set, "Error updating the DockStatAPI config", - error as string, + error as string ); } }, @@ -88,7 +88,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Update the current DockStatAPI config", }, - }, + } ) .get( "/package", @@ -110,7 +110,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error while reading package.json", + "Error while reading package.json" ); } }, @@ -119,5 +119,5 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Returns relevant information about the package.json", }, - }, + } ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 6db7362e..dcf97a73 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -1,7 +1,7 @@ import { Elysia, t } from "elysia"; import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/respone-handler"; +import { responseHandler } from "~/core/utils/response-handler"; export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) .post( @@ -26,7 +26,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) }, body: t.Object({ name: t.String(), - hostadress: t.String(), + hostAddress: t.String(), secure: t.Boolean(), }), } @@ -54,7 +54,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) body: t.Object({ id: t.Number(), name: t.String(), - hostadress: t.String(), + hostAddress: t.String(), secure: t.Boolean(), }), } diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 4324fef5..c86f6883 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -7,7 +7,7 @@ import { calculateMemoryUsage, } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/respone-handler"; +import { responseHandler } from "~/core/utils/response-handler"; import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index bd9571b7..c4444b4d 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -7,7 +7,7 @@ import { calculateMemoryUsage, } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/respone-handler"; +import { responseHandler } from "~/core/utils/response-handler"; import split2 from "split2"; import type { Readable } from "stream"; @@ -60,13 +60,15 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( .on("close", () => splitStream.destroy()) .pipe(splitStream) .on("data", (line: string) => { - if (ws.readyState !== 1 || !line) return; + if (ws.readyState !== 1 || !line) { + return; + } try { const stats = JSON.parse(line); ws.send( JSON.stringify({ id: containerInfo.Id, - hostId: host.id as string, + hostId: host.id, name: containerInfo.Names[0].replace(/^\//, ""), image: containerInfo.Image, status: containerInfo.Status, diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index c4e6c2cb..7c594d19 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -1,5 +1,5 @@ import { Elysia, t } from "elysia"; -import { responseHandler } from "~/core/utils/respone-handler"; +import { responseHandler } from "~/core/utils/response-handler"; import { deployStack, stopStack, @@ -48,18 +48,18 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body.automatic_reboot_on_error, isCustom, image_updates, - body.stack_prefix, + body.stack_prefix ); logger.info(`Deployed Stack (${body.name})`); return responseHandler.ok( set, - `Stack ${body.name} deployed successfully`, + `Stack ${body.name} deployed successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error deploying stack", + "Error deploying stack" ); } }, @@ -79,7 +79,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) source: t.String(), stack_prefix: t.Optional(t.String()), }), - }, + } ) .post( "/start", @@ -92,13 +92,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Started Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} started successfully`, + `Stack ${body.stack} started successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error starting stack", + "Error starting stack" ); } }, @@ -107,7 +107,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .post( "/stop", @@ -120,13 +120,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Stopped Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} stopped successfully`, + `Stack ${body.stack} stopped successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error stopping stack", + "Error stopping stack" ); } }, @@ -135,7 +135,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .post( "/restart", @@ -148,13 +148,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Restarted Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} restarted successfully`, + `Stack ${body.stack} restarted successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error restarting stack", + "Error restarting stack" ); } }, @@ -163,7 +163,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .post( "/pull-images", @@ -176,13 +176,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Pulled Stack images (${body.stack})`); return responseHandler.ok( set, - `Images for stack ${body.stack} pulled successfully`, + `Images for stack ${body.stack} pulled successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error pulling images", + "Error pulling images" ); } }, @@ -194,7 +194,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - }, + } ) .get( "/status", @@ -206,7 +206,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) status = await getStackStatus(query.stack_name); res = responseHandler.ok( set, - `Stack ${query.stack_name} status retrieved successfully`, + `Stack ${query.stack_name} status retrieved successfully` ); logger.info("Fetched Stack status"); } else { @@ -219,7 +219,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stack status", + "Error getting stack status" ); } }, @@ -232,7 +232,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) query: t.Object({ stack_name: t.Any(), }), - }, + } ) .get( "/", @@ -245,7 +245,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stacks", + "Error getting stacks" ); } }, @@ -254,5 +254,5 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Returns an Array of Stack-config-objects", }, - }, + } ); diff --git a/src/routes/utils.ts b/src/routes/utils.ts index 22534244..cb8b9424 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -10,7 +10,7 @@ import { devDependencies, license, } from "~/core/utils/package-json"; -import { responseHandler } from "~/core/utils/respone-handler"; +import { responseHandler } from "~/core/utils/response-handler"; export const utilRoutes = new Elysia({ prefix: "/utils" }).get( "/info", diff --git a/src/tests/post.spec.ts b/src/tests/post.spec.ts index f581770d..040012e3 100644 --- a/src/tests/post.spec.ts +++ b/src/tests/post.spec.ts @@ -6,7 +6,7 @@ describe("DockStatAPI (POST)", () => { it("Check Host adding", async () => { const body = { name: "test", - hostadress: "localhost:2375", + hostAddress: "localhost:2375", secure: false, }; @@ -18,18 +18,18 @@ describe("DockStatAPI (POST)", () => { const codeBody: DockerHost = { id: 2, name: "test", - hostadress: "127.0.0.1:2375", + hostAddress: "127.0.0.1:2375", secure: false, }; await runTestCode("/docker-config/update-host", 200, "POST", codeBody); const responseBody: DockerHost[] = [ - { id: 2, name: "test", hostadress: "127.0.0.1:2375", secure: 0 }, + { id: 2, name: "test", hostAddress: "127.0.0.1:2375", secure: 0 }, { id: 1, name: "Localhost", - hostadress: "localhost:2375", + hostAddress: "localhost:2375", secure: 0, }, ]; diff --git a/src/typings/docker.ts b/src/typings/docker.ts index f295ab39..e6701f6c 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -1,6 +1,6 @@ interface DockerHost { name: string; - hostadress: string; + hostAddress: string; secure: boolean | number; id?: number; } From df7b2b659aa2ec255dc980c5f88d58f1576015e4 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 21 Mar 2025 19:18:33 +0000 Subject: [PATCH 220/369] Update dependency graphs --- dependency-graph.dot | 18 +++++----- dependency-graph.mmd | 2 +- dependency-graph.svg | 86 ++++++++++++++++++++++---------------------- 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 48a4a931..21ecb014 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -111,9 +111,9 @@ strict digraph "dependency-cruiser output"{ "src/core/utils/logger.ts" -> "path" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/package-json.ts" [label= tooltip="package-json.ts" URL="src/core/utils/package-json.ts" fillcolor="#ddfeff"] } } } "src/core/utils/package-json.ts" -> "package.json" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/respone-handler.ts" [label= tooltip="respone-handler.ts" URL="src/core/utils/respone-handler.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/respone-handler.ts" -> "src/core/utils/logger.ts" - "src/core/utils/respone-handler.ts" -> "src/typings/elysiajs.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/response-handler.ts" [label= tooltip="response-handler.ts" URL="src/core/utils/response-handler.ts" fillcolor="#ddfeff"] } } } + "src/core/utils/response-handler.ts" -> "src/core/utils/logger.ts" + "src/core/utils/response-handler.ts" -> "src/typings/elysiajs.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } "src/index.ts" -> "src/core/docker/monitor.ts" "src/index.ts" -> "src/middleware/auth.ts" @@ -141,19 +141,19 @@ strict digraph "dependency-cruiser output"{ "src/routes/api-config.ts" -> "src/core/plugins/plugin-manager.ts" "src/routes/api-config.ts" -> "src/core/utils/logger.ts" "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" - "src/routes/api-config.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/api-config.ts" -> "src/core/utils/response-handler.ts" "src/routes/api-config.ts" -> "src/middleware/auth.ts" "src/routes/api-config.ts" -> "src/typings/database.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-manager.ts" [label= tooltip="docker-manager.ts" URL="src/routes/docker-manager.ts" fillcolor="#ddfeff"] } } "src/routes/docker-manager.ts" -> "src/core/database/repository.ts" "src/routes/docker-manager.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-manager.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/docker-manager.ts" -> "src/core/utils/response-handler.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-stats.ts" [label= tooltip="docker-stats.ts" URL="src/routes/docker-stats.ts" fillcolor="#ddfeff"] } } "src/routes/docker-stats.ts" -> "src/core/database/repository.ts" "src/routes/docker-stats.ts" -> "src/core/docker/client.ts" "src/routes/docker-stats.ts" -> "src/core/utils/calculations.ts" "src/routes/docker-stats.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-stats.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/docker-stats.ts" -> "src/core/utils/response-handler.ts" "src/routes/docker-stats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] "src/routes/docker-stats.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-websocket.ts" [label= tooltip="docker-websocket.ts" URL="src/routes/docker-websocket.ts" fillcolor="#ddfeff"] } } @@ -161,7 +161,7 @@ strict digraph "dependency-cruiser output"{ "src/routes/docker-websocket.ts" -> "src/core/docker/client.ts" "src/routes/docker-websocket.ts" -> "src/core/utils/calculations.ts" "src/routes/docker-websocket.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-websocket.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/docker-websocket.ts" -> "src/core/utils/response-handler.ts" "src/routes/docker-websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/live-logs.ts" [label= tooltip="live-logs.ts" URL="src/routes/live-logs.ts" fillcolor="#ddfeff"] } } "src/routes/live-logs.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] @@ -173,10 +173,10 @@ strict digraph "dependency-cruiser output"{ "src/routes/stacks.ts" -> "src/core/database/repository.ts" "src/routes/stacks.ts" -> "src/core/stacks/controller.ts" "src/routes/stacks.ts" -> "src/core/utils/logger.ts" - "src/routes/stacks.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/stacks.ts" -> "src/core/utils/response-handler.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/utils.ts" [label= tooltip="utils.ts" URL="src/routes/utils.ts" fillcolor="#ddfeff"] } } "src/routes/utils.ts" -> "src/core/utils/package-json.ts" - "src/routes/utils.ts" -> "src/core/utils/respone-handler.ts" + "src/routes/utils.ts" -> "src/core/utils/response-handler.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/database.ts" [label= tooltip="database.ts" URL="src/typings/database.ts" fillcolor="#ddfeff"] } } subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker-compose.ts" [label= tooltip="docker-compose.ts" URL="src/typings/docker-compose.ts" fillcolor="#ddfeff"] } } subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker.ts" [label= tooltip="docker.ts" URL="src/typings/docker.ts" fillcolor="#ddfeff"] } } diff --git a/dependency-graph.mmd b/dependency-graph.mmd index b845e1e8..da1b20e5 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -22,7 +22,7 @@ subgraph 6["plugins"] end subgraph 9["utils"] A["logger.ts"] -W["respone-handler.ts"] +W["response-handler.ts"] Y["package-json.ts"] 14["calculations.ts"] 17["change-me-checker.ts"] diff --git a/dependency-graph.svg b/dependency-graph.svg index 412e5586..b5a3b806 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -215,8 +215,8 @@ src/core/utils/logger.ts->src/typings/websocket.ts - - + + @@ -558,8 +558,8 @@ src/core/utils/change-me-checker.ts->fs/promises - - + + @@ -911,20 +911,20 @@ - + -src/core/utils/respone-handler.ts - - -respone-handler.ts +src/core/utils/response-handler.ts + + +response-handler.ts - + -src/core/utils/respone-handler.ts->src/core/utils/logger.ts - - +src/core/utils/response-handler.ts->src/core/utils/logger.ts + + @@ -935,11 +935,11 @@ - + -src/core/utils/respone-handler.ts->src/typings/elysiajs.ts - - +src/core/utils/response-handler.ts->src/typings/elysiajs.ts + + @@ -1121,8 +1121,8 @@ src/middleware/auth.ts->src/core/utils/logger.ts - - + + @@ -1139,8 +1139,8 @@ src/middleware/auth.ts->src/typings/elysiajs.ts - - + + @@ -1160,11 +1160,11 @@ - + -src/routes/stacks.ts->src/core/utils/respone-handler.ts - - +src/routes/stacks.ts->src/core/utils/response-handler.ts + + @@ -1172,11 +1172,11 @@ - + -src/routes/utils.ts->src/core/utils/respone-handler.ts - - +src/routes/utils.ts->src/core/utils/response-handler.ts + + @@ -1208,11 +1208,11 @@ - + -src/routes/api-config.ts->src/core/utils/respone-handler.ts - - +src/routes/api-config.ts->src/core/utils/response-handler.ts + + @@ -1232,9 +1232,9 @@ - + -src/routes/docker-manager.ts->src/core/utils/respone-handler.ts +src/routes/docker-manager.ts->src/core/utils/response-handler.ts @@ -1274,11 +1274,11 @@ - + -src/routes/docker-stats.ts->src/core/utils/respone-handler.ts - - +src/routes/docker-stats.ts->src/core/utils/response-handler.ts + + @@ -1304,11 +1304,11 @@ - + -src/routes/docker-websocket.ts->src/core/utils/respone-handler.ts - - +src/routes/docker-websocket.ts->src/core/utils/response-handler.ts + + From 690ab3c5f7bd92daa9359c51c0ffa96b66b813ee Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 21 Mar 2025 20:28:43 +0100 Subject: [PATCH 221/369] Fix: Knip adjustment --- .knip.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.knip.json b/.knip.json index 64b44a6d..0c1cd545 100644 --- a/.knip.json +++ b/.knip.json @@ -1,5 +1,5 @@ { "entry": ["src/index.ts"], "project": ["src/**/*.ts"], - "ignore": ["src/plugins/*.plugin.ts"] + "ignore": ["src/plugins/*.plugin.ts","src/tests/*.ts"] } From 6118ad90c764d1078ed147e6e014ecd9560b3dcc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 26 Mar 2025 16:37:41 +0100 Subject: [PATCH 222/369] Fix: Minor code adjustments --- .github/workflows/{docker.yaml => pipeline.yaml} | 4 +--- docker/Dockerfile | 2 +- package.json | 2 +- src/typings/websocket.ts | 14 +++++++------- 4 files changed, 10 insertions(+), 12 deletions(-) rename .github/workflows/{docker.yaml => pipeline.yaml} (97%) diff --git a/.github/workflows/docker.yaml b/.github/workflows/pipeline.yaml similarity index 97% rename from .github/workflows/docker.yaml rename to .github/workflows/pipeline.yaml index b917f7f7..c4ddc1f3 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/pipeline.yaml @@ -25,15 +25,13 @@ jobs: - name: Start proxy run: | - pwd - ls -lah docker compose -f docker/docker-compose.dev.yaml up -d - name: Run Unit-tests run: | bun install bun clean - bun test + bun test --reporter=junit --reporter-outfile=./bun.xml - name: Run Docker Build run: | diff --git a/docker/Dockerfile b/docker/Dockerfile index da820571..bc09e1bf 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -23,7 +23,7 @@ LABEL org.opencontainers.image.title="DockStatAPI" \ RUN apk add --no-cache curl -HEALTHCHECK --timeout=30s --start-period=5s --retries=3 \ +HEALTHCHECK --timeout=10s --start-period=2s --retries=3 \ CMD curl --fail http://localhost:3000/health || exit 1 VOLUME [ "/DockStatAPI/src/plugins" ] diff --git a/package.json b/package.json index 3a1dd228..b024205c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "version": "3.0.0", "scripts": { "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", - "start:docker": "bun run build:docker && docker run -p 3001:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", + "start:docker": "bun run build:docker && docker run -p 3000:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", "build": "bun build --target bun src/index.ts --outdir ./dist", diff --git a/src/typings/websocket.ts b/src/typings/websocket.ts index 59f5e3a3..1cb3821f 100644 --- a/src/typings/websocket.ts +++ b/src/typings/websocket.ts @@ -1,10 +1,10 @@ -import type { Readable, Transform } from "stream"; -import type internal from "stream"; +//import type { Readable, Transform } from "stream"; +//import type internal from "stream"; -interface streams { - statsStream: Readable; - splitStream: internal.Transform; -} +//interface streams { +// statsStream: Readable; +// splitStream: internal.Transform; +//} interface logStreamData { timestamp: string; @@ -14,4 +14,4 @@ interface logStreamData { line: number; } -export { streams, logStreamData }; +export { logStreamData }; From 9b9945c597aa6a7e1417816b674d032bf21b1fc4 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 26 Mar 2025 15:38:15 +0000 Subject: [PATCH 223/369] Update dependency graphs --- dependency-graph.dot | 1 - dependency-graph.mmd | 243 +++++++------ dependency-graph.svg | 832 +++++++++++++++++++++---------------------- 3 files changed, 534 insertions(+), 542 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 21ecb014..254698fb 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -185,6 +185,5 @@ strict digraph "dependency-cruiser output"{ subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/plugin.ts" [label= tooltip="plugin.ts" URL="src/typings/plugin.ts" fillcolor="#ddfeff"] } } "src/typings/plugin.ts" -> "src/typings/docker.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/websocket.ts" [label= tooltip="websocket.ts" URL="src/typings/websocket.ts" fillcolor="#ddfeff"] } } - "src/typings/websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] "stream" [label= tooltip="stream" URL="https://nodejs.org/api/stream.html" color="grey" fontcolor="grey"] } diff --git a/dependency-graph.mmd b/dependency-graph.mmd index da1b20e5..ad13d50c 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -11,100 +11,100 @@ subgraph 0["src"] subgraph 2["core"] subgraph 3["docker"] 4["monitor.ts"] -O["client.ts"] -10["scheduler.ts"] -11["store-host-stats.ts"] -13["store-container-stats.ts"] +N["client.ts"] +Z["scheduler.ts"] +10["store-host-stats.ts"] +12["store-container-stats.ts"] end subgraph 6["plugins"] 7["plugin-manager.ts"] -15["loader.ts"] +14["loader.ts"] end subgraph 9["utils"] A["logger.ts"] -W["response-handler.ts"] -Y["package-json.ts"] -14["calculations.ts"] -17["change-me-checker.ts"] +V["response-handler.ts"] +X["package-json.ts"] +13["calculations.ts"] +16["change-me-checker.ts"] end subgraph C["database"] D["repository.ts"] F["helper.ts"] end -subgraph T["stacks"] -U["controller.ts"] +subgraph S["stacks"] +T["controller.ts"] end -subgraph 19["trpc"] -1A["index.ts"] -1B["router.ts"] -subgraph 1C["procedures"] -1D["api-config.procedure.ts"] -1F["docker-manager.procedure.ts"] -1G["docker-stats.procedure.ts"] -1H["logs.procedure.ts"] -1I["stacks.procedure.ts"] +subgraph 18["trpc"] +19["index.ts"] +1A["router.ts"] +subgraph 1B["procedures"] +1C["api-config.procedure.ts"] +1E["docker-manager.procedure.ts"] +1F["docker-stats.procedure.ts"] +1G["logs.procedure.ts"] +1H["stacks.procedure.ts"] end -1E["trpc.ts"] +1D["trpc.ts"] end end subgraph G["typings"] H["database.ts"] I["docker.ts"] L["websocket.ts"] -N["plugin.ts"] -R["elysiajs.ts"] -V["docker-compose.ts"] -12["dockerode.ts"] +M["plugin.ts"] +Q["elysiajs.ts"] +U["docker-compose.ts"] +11["dockerode.ts"] end subgraph J["routes"] K["live-logs.ts"] -S["stacks.ts"] -X["utils.ts"] -1J["api-config.ts"] -1K["docker-manager.ts"] -1L["docker-stats.ts"] -1M["docker-websocket.ts"] +R["stacks.ts"] +W["utils.ts"] +1I["api-config.ts"] +1J["docker-manager.ts"] +1K["docker-stats.ts"] +1L["docker-websocket.ts"] 1N["logs.ts"] end -subgraph P["middleware"] -Q["auth.ts"] +subgraph O["middleware"] +P["auth.ts"] end end 5["bun"] 8["events"] B["path"] E["bun:sqlite"] -M["stream"] -Z["package.json"] -subgraph 16["fs"] -18["promises"] +Y["package.json"] +subgraph 15["fs"] +17["promises"] end +1M["stream"] 1-->4 -1-->Q +1-->P 1-->K -1-->S -1-->X +1-->R +1-->W 1-->H 1-->D -1-->10 -1-->15 -1-->1A +1-->Z +1-->14 +1-->19 1-->A +1-->1I 1-->1J 1-->1K 1-->1L -1-->1M 1-->1N 4-->7 4-->D -4-->O +4-->N 4-->A 4-->I 4-->I 4-->5 7-->A 7-->I -7-->N +7-->M 7-->8 A-->D A-->K @@ -118,101 +118,100 @@ D-->E F-->A K-->A K-->L -L-->M +M-->I +N-->A N-->I -O-->A -O-->I -Q-->D -Q-->A -Q-->H -Q-->R -S-->D -S-->U -S-->A -S-->W -U-->D -U-->A -U-->H -U-->V -W-->A -W-->R +P-->D +P-->A +P-->H +P-->Q +R-->D +R-->T +R-->A +R-->V +T-->D +T-->A +T-->H +T-->U +V-->A +V-->Q +W-->X +W-->V X-->Y -X-->W -Y-->Z +Z-->D +Z-->10 +Z-->12 +Z-->A +Z-->H 10-->D -10-->11 -10-->13 +10-->N 10-->A -10-->H -11-->D -11-->O -11-->A -11-->I -11-->12 -13-->A -13-->D -13-->O -13-->14 -15-->17 -15-->A -15-->7 -15-->16 -15-->B -17-->A -17-->18 -1A-->1B -1B-->1D -1B-->1F -1B-->1G -1B-->1H -1B-->1I -1B-->1E -1D-->1E -1D-->D -1D-->A -1D-->Y -1D-->H -1F-->1E +10-->I +10-->11 +12-->A +12-->D +12-->N +12-->13 +14-->16 +14-->A +14-->7 +14-->15 +14-->B +16-->A +16-->17 +19-->1A +1A-->1C +1A-->1E +1A-->1F +1A-->1G +1A-->1H +1A-->1D +1C-->1D +1C-->D +1C-->A +1C-->X +1C-->H +1E-->1D +1E-->D +1E-->A +1E-->I +1F-->1D 1F-->D +1F-->N +1F-->13 1F-->A 1F-->I -1G-->1E +1F-->11 +1G-->1D 1G-->D -1G-->O -1G-->14 1G-->A -1G-->I -1G-->12 -1H-->1E +1H-->1D 1H-->D +1H-->T 1H-->A -1I-->1E 1I-->D -1I-->U +1I-->7 1I-->A +1I-->X +1I-->V +1I-->P +1I-->H 1J-->D -1J-->7 1J-->A -1J-->Y -1J-->W -1J-->Q -1J-->H +1J-->V 1K-->D +1K-->N +1K-->13 1K-->A -1K-->W +1K-->V +1K-->I +1K-->11 1L-->D -1L-->O -1L-->14 +1L-->N +1L-->13 1L-->A -1L-->W -1L-->I -1L-->12 -1M-->D -1M-->O -1M-->14 -1M-->A -1M-->W -1M-->M +1L-->V +1L-->1M 1N-->D 1N-->A diff --git a/dependency-graph.svg b/dependency-graph.svg index b5a3b806..715f66a2 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,82 +4,82 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/trpc - -trpc + +trpc cluster_src/core/trpc/procedures - -procedures + +procedures cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -87,8 +87,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -96,8 +96,8 @@ events - -events + +events @@ -105,8 +105,8 @@ fs - -fs + +fs @@ -114,8 +114,8 @@ fs/promises - -promises + +promises @@ -123,8 +123,8 @@ package.json - -package.json + +package.json @@ -132,8 +132,8 @@ path - -path + +path @@ -141,8 +141,8 @@ src/core/database/helper.ts - -helper.ts + +helper.ts @@ -150,477 +150,477 @@ src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/utils/logger.ts->path - - + + src/core/database/repository.ts - -repository.ts + +repository.ts src/core/utils/logger.ts->src/core/database/repository.ts - - - - + + + + src/routes/live-logs.ts - -live-logs.ts + +live-logs.ts src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/typings/websocket.ts - -websocket.ts + +websocket.ts src/core/utils/logger.ts->src/typings/websocket.ts - - + + src/core/database/repository.ts->bun:sqlite - - + + src/core/database/repository.ts->src/core/database/helper.ts - - - - + + + + src/core/database/repository.ts->src/core/utils/logger.ts - - - - + + + + src/typings/database.ts - -database.ts + +database.ts src/core/database/repository.ts->src/typings/database.ts - - + + src/typings/docker.ts - -docker.ts + +docker.ts src/core/database/repository.ts->src/typings/docker.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->bun - - + + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->src/core/database/repository.ts - - + + src/core/docker/monitor.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + src/core/plugins/plugin-manager.ts->events - - + + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + src/typings/plugin.ts - -plugin.ts + +plugin.ts src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/repository.ts - - + + src/core/docker/scheduler.ts->src/typings/database.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + src/typings/dockerode.ts - -dockerode.ts + +dockerode.ts src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + src/core/utils/change-me-checker.ts->fs/promises - - + + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts->src/core/database/repository.ts - - + + src/core/stacks/controller.ts->src/typings/database.ts - - + + src/typings/docker-compose.ts - -docker-compose.ts + +docker-compose.ts src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + src/core/trpc/index.ts - -index.ts + +index.ts @@ -628,705 +628,699 @@ src/core/trpc/router.ts - -router.ts + +router.ts src/core/trpc/index.ts->src/core/trpc/router.ts - - + + src/core/trpc/procedures/api-config.procedure.ts - -api-config.procedure.ts + +api-config.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - + + src/core/trpc/trpc.ts - -trpc.ts + +trpc.ts src/core/trpc/router.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts - -docker-manager.procedure.ts + +docker-manager.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts - -docker-stats.procedure.ts + +docker-stats.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - + + src/core/trpc/procedures/logs.procedure.ts - -logs.procedure.ts + +logs.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - + + src/core/trpc/procedures/stacks.procedure.ts - -stacks.procedure.ts + +stacks.procedure.ts src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - + + src/core/utils/package-json.ts->package.json - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/typings/docker.ts - - + + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - + + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - + + src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + src/routes/live-logs.ts->src/typings/websocket.ts - - - - - -stream - - -stream - - - - - -src/typings/websocket.ts->stream - - + + src/core/utils/response-handler.ts - -response-handler.ts + +response-handler.ts src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + src/typings/elysiajs.ts - -elysiajs.ts + +elysiajs.ts src/core/utils/response-handler.ts->src/typings/elysiajs.ts - - + + src/index.ts - -index.ts + +index.ts src/index.ts->src/core/utils/logger.ts - - + + src/index.ts->src/core/database/repository.ts - - + + src/index.ts->src/typings/database.ts - - + + src/index.ts->src/core/docker/monitor.ts - - + + src/index.ts->src/core/docker/scheduler.ts - - + + src/index.ts->src/core/plugins/loader.ts - - + + src/index.ts->src/core/trpc/index.ts - - + + src/index.ts->src/routes/live-logs.ts - - + + src/middleware/auth.ts - -auth.ts + +auth.ts src/index.ts->src/middleware/auth.ts - - + + src/routes/stacks.ts - -stacks.ts + +stacks.ts src/index.ts->src/routes/stacks.ts - - + + src/routes/utils.ts - -utils.ts + +utils.ts src/index.ts->src/routes/utils.ts - - + + src/routes/api-config.ts - -api-config.ts + +api-config.ts src/index.ts->src/routes/api-config.ts - - + + src/routes/docker-manager.ts - -docker-manager.ts + +docker-manager.ts src/index.ts->src/routes/docker-manager.ts - - + + src/routes/docker-stats.ts - -docker-stats.ts + +docker-stats.ts src/index.ts->src/routes/docker-stats.ts - - + + src/routes/docker-websocket.ts - -docker-websocket.ts + +docker-websocket.ts src/index.ts->src/routes/docker-websocket.ts - - + + src/routes/logs.ts - -logs.ts + +logs.ts src/index.ts->src/routes/logs.ts - - + + src/middleware/auth.ts->src/core/utils/logger.ts - - + + src/middleware/auth.ts->src/core/database/repository.ts - - + + src/middleware/auth.ts->src/typings/database.ts - - + + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + src/routes/stacks.ts->src/core/utils/logger.ts - - + + src/routes/stacks.ts->src/core/database/repository.ts - - + + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + src/routes/utils.ts->src/core/utils/package-json.ts - - + + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + src/routes/api-config.ts->src/core/utils/logger.ts - - + + src/routes/api-config.ts->src/core/database/repository.ts - - + + src/routes/api-config.ts->src/typings/database.ts - - + + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + src/routes/api-config.ts->src/middleware/auth.ts - - + + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + src/routes/docker-manager.ts->src/core/database/repository.ts - - + + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + src/routes/docker-stats.ts->src/core/database/repository.ts - - + + src/routes/docker-stats.ts->src/typings/docker.ts - - + + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + src/routes/docker-websocket.ts->src/core/database/repository.ts - - + + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + + + + +stream + + +stream + + src/routes/docker-websocket.ts->stream - - + + src/routes/logs.ts->src/core/utils/logger.ts - - + + src/routes/logs.ts->src/core/database/repository.ts - - + + From 26fac15b4ed540965c7b00cee6ad52d7c5036a38 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 27 Mar 2025 10:36:20 +0100 Subject: [PATCH 224/369] Fix: WebSocket connection show all containers --- src/core/utils/calculations.ts | 17 +++++++++++++++-- src/routes/docker-websocket.ts | 20 ++++++++++---------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index 9428bf98..3f12f956 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -19,14 +19,27 @@ const calculateCpuPercent = (stats: Docker.ContainerStats): number => { return 0; } - return (cpuDelta / systemDelta) * 100; + const data = (cpuDelta / systemDelta) * 100; + + if (data === null) { + return 0; + } + + return data; }; const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { if (stats == null) { return 0.0; } - return (stats.memory_stats.usage / stats.memory_stats.limit) * 100; + + const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; + + if (data === null) { + return 0; + } + + return data; }; export { calculateCpuPercent, calculateMemoryUsage }; diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index c4444b4d..b3a6c52b 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -38,9 +38,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( const docker = getDockerClient(host); await docker.ping(); - const containers = await docker.listContainers(); + const containers = await docker.listContainers({ all: true }); logger.debug( - `Found ${containers.length} containers on ${host.name} (id: ${host.id})` + `Found ${containers.length} containers on ${host.name} (id: ${host.id})`, ); for (const containerInfo of containers) { @@ -73,9 +73,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( image: containerInfo.Image, status: containerInfo.Status, state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - }) + cpuUsage: calculateCpuPercent(stats) || 0, + memoryUsage: calculateMemoryUsage(stats) || 0, + }), ); } catch (error) { logger.error(`Parse error: ${error}`); @@ -89,7 +89,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( hostId: host.name, containerId: containerInfo.Id, error: `Stats stream error: ${error}`, - }) + }), ); }); } @@ -102,9 +102,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( { headers: {} }, error as string, "Docker connection failed", - 500 - ) - ) + 500, + ), + ), ); } }, @@ -129,5 +129,5 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( }); connectionStreams.delete(ws); }, - } + }, ); From c1f577d2c4da561363ecb67530664c3de43e803c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 29 Mar 2025 19:39:19 +0100 Subject: [PATCH 225/369] Feat: More IDs and smaller changes --- docker/docker-compose.dev.yaml | 2 +- src/core/database/repository.ts | 96 +++++++++++++++-------------- src/core/docker/store-host-stats.ts | 19 +++++- src/core/stacks/controller.ts | 47 +++++++++----- src/routes/docker-manager.ts | 44 ++++++++++--- src/routes/stacks.ts | 42 ++++++------- src/typings/database.ts | 1 + src/typings/docker-compose.ts | 1 + src/typings/docker.ts | 5 +- 9 files changed, 163 insertions(+), 94 deletions(-) diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index cca8f34a..878d1f0a 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -42,7 +42,7 @@ services: - VOLUMES=1 #optional sqlite-web: - container_name: qlite-web + container_name: sqlite-web image: ghcr.io/coleifer/sqlite-web:latest ports: - 8080:8080 diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts index a6dfaded..1c920a74 100644 --- a/src/core/database/repository.ts +++ b/src/core/database/repository.ts @@ -20,7 +20,8 @@ export const dbFunctions = { ); CREATE TABLE IF NOT EXISTS stacks_config ( - name TEXT PRIMARY KEY NOT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, version INTEGER NOT NULL, custom BOOLEAN NOT NULL, source TEXT NOT NULL, @@ -38,20 +39,21 @@ export const dbFunctions = { ); CREATE TABLE IF NOT EXISTS host_stats ( - hostId TEXT PRIMARY KEY NOT NULL, - dockerVersion TEXT NOT NULL, - apiVersion TEXT NOT NULL, - os TEXT NOT NULL, - architecture TEXT NOT NULL, - totalMemory INTEGER NOT NULL, - totalCPU INTEGER NOT NULL, - labels TEXT NOT NULL, - containers INTEGER NOT NULL, - containersRunning INTEGER NOT NULL, - containersStopped INTEGER NOT NULL, - containersPaused INTEGER NOT NULL, - images INTEGER NOT NULL - ); + hostId INTEGER PRIMARY KEY NOT NULL, + hostName TEXT NOT NULL, + dockerVersion TEXT NOT NULL, + apiVersion TEXT NOT NULL, + os TEXT NOT NULL, + architecture TEXT NOT NULL, + totalMemory INTEGER NOT NULL, + totalCPU INTEGER NOT NULL, + labels TEXT NOT NULL, + containers INTEGER NOT NULL, + containersRunning INTEGER NOT NULL, + containersStopped INTEGER NOT NULL, + containersPaused INTEGER NOT NULL, + images INTEGER NOT NULL + ); CREATE TABLE IF NOT EXISTS container_stats ( id TEXT NOT NULL, @@ -88,7 +90,7 @@ export const dbFunctions = { const stmt = db.prepare( ` INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme") - ` + `, ); stmt.run(); } @@ -101,7 +103,7 @@ export const dbFunctions = { const stmt = db.prepare( ` INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?) - ` + `, ); stmt.run("Localhost", "localhost:2375", false); } @@ -139,7 +141,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addDockerHost"); throw new TypeError("Invalid parameter types for addDockerHost"); } - } + }, ); }, @@ -154,7 +156,7 @@ export const dbFunctions = { `); return stmt.all() as DockerHost[]; }, - () => {} + () => {}, ); }, @@ -162,7 +164,7 @@ export const dbFunctions = { level: string, message: string, file_name: string, - line: number + line: number, ) => { if ( typeof level !== "string" || @@ -192,7 +194,7 @@ export const dbFunctions = { `); return stmt.all(); }, - () => {} + () => {}, ); }, @@ -213,7 +215,7 @@ export const dbFunctions = { logger.error("Level parameter must be a string"); throw new TypeError("Level parameter must be a string"); } - } + }, ); }, @@ -230,7 +232,7 @@ export const dbFunctions = { host.hostAddress, host.secure, host.name, - String(host.id) + String(host.id), ); }, () => { @@ -243,7 +245,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateDockerHost"); throw new TypeError("Invalid parameter types for updateDockerHost"); } - } + }, ); }, @@ -262,7 +264,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteDockerHost"); throw new TypeError("id parameter must be a number"); } - } + }, ); }, @@ -275,7 +277,7 @@ export const dbFunctions = { `); return stmt.run(); }, - () => {} + () => {}, ); }, @@ -294,14 +296,14 @@ export const dbFunctions = { logger.error("Invalid parameter type for clearLogsByLevel"); throw new TypeError("Level parameter must be a string"); } - } + }, ); }, updateConfig( fetching_interval: number, keep_data_for: number, - api_key: string + api_key: string, ) { return executeDbOperation( "Update Config", @@ -322,7 +324,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for updateConfig"); throw new TypeError("Invalid parameter types for updateConfig"); } - } + }, ); }, @@ -336,7 +338,7 @@ export const dbFunctions = { `); return stmt.all(); }, - () => {} + () => {}, ); }, @@ -361,7 +363,7 @@ export const dbFunctions = { logger.error("Invalid parameter type for deleteOldData"); throw new TypeError("Days parameter must be a number"); } - } + }, ); }, @@ -373,7 +375,7 @@ export const dbFunctions = { status: string, state: string, cpu_usage: number, - memory_usage: number + memory_usage: number, ) { return executeDbOperation( "Add Container Stats", @@ -390,7 +392,7 @@ export const dbFunctions = { status, state, cpu_usage, - memory_usage + memory_usage, ); }, () => { @@ -407,7 +409,7 @@ export const dbFunctions = { logger.error("Invalid parameter types for addContainerStats"); throw new TypeError("Invalid parameter types for addContainerStats"); } - } + }, ); }, @@ -419,6 +421,7 @@ export const dbFunctions = { const stmt = db.prepare(` INSERT INTO host_stats ( hostId, + hostName, dockerVersion, apiVersion, os, @@ -432,7 +435,7 @@ export const dbFunctions = { containersPaused, images ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(hostId) DO UPDATE SET dockerVersion = excluded.dockerVersion, apiVersion = excluded.apiVersion, @@ -449,6 +452,7 @@ export const dbFunctions = { `); return stmt.run( stats.hostId, + stats.hostName, stats.dockerVersion, stats.apiVersion, stats.os, @@ -460,10 +464,10 @@ export const dbFunctions = { stats.containersRunning, stats.containersStopped, stats.containersPaused, - stats.images + stats.images, ); }, - () => {} + () => {}, ); }, @@ -492,10 +496,10 @@ export const dbFunctions = { stack_config.container_count, stack_config.stack_prefix, stack_config.automatic_reboot_on_error, - stack_config.image_updates + stack_config.image_updates, ); }, - () => {} + () => {}, ); }, @@ -510,21 +514,21 @@ export const dbFunctions = { `); return stmt.all(); }, - () => {} + () => {}, ); }, - deleteStack(name: string) { + deleteStack(id: number) { return executeDbOperation( "Delete Stack", () => { const stmt = db.prepare(` DELETE FROM stacks_config - WHERE name = ?; + WHERE id = ?; `); - return stmt.run(name); + return stmt.run(id); }, - () => {} + () => {}, ); }, @@ -552,10 +556,10 @@ export const dbFunctions = { stack_config.stack_prefix, stack_config.automatic_reboot_on_error, stack_config.image_updates, - stack_config.name + stack_config.name, ); }, - () => {} + () => {}, ); }, }; diff --git a/src/core/docker/store-host-stats.ts b/src/core/docker/store-host-stats.ts index f8cd3527..a7fe6e15 100644 --- a/src/core/docker/store-host-stats.ts +++ b/src/core/docker/store-host-stats.ts @@ -4,6 +4,15 @@ import { DockerHost, HostStats } from "~/typings/docker"; import { getDockerClient } from "~/core/docker/client"; import { DockerInfo } from "~/typings/dockerode"; +function getHostByName(hostName: string): DockerHost { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const foundHost = hosts.find((host) => host.name === hostName); + if (!foundHost) { + throw new Error(`Host ${hostName} not found`); + } + return foundHost; +} + async function storeHostData() { try { const hosts = dbFunctions.getDockerHosts() as DockerHost[]; @@ -22,7 +31,6 @@ async function storeHostData() { } let hostStats: DockerInfo; - let stats: HostStats; try { hostStats = await docker.info(); } catch (error) { @@ -32,9 +40,16 @@ async function storeHostData() { ); } + const hostId = getHostByName(host.name).id; + + if (!hostId) { + throw new Error(`Host "${host.name}" not found`); + } + try { const stats: HostStats = { - hostId: host.name, + hostId: hostId, + hostName: host.name, dockerVersion: hostStats.ServerVersion, apiVersion: hostStats.Driver, os: hostStats.OperatingSystem, diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index f4af21a9..1f2e0341 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -6,17 +6,17 @@ import type { Stack, ComposeSpec } from "~/typings/docker-compose"; import type { stacks_config } from "~/typings/database"; async function runStackCommand( - stack_name: string, + stack_id: number, command: (cwd: string) => Promise, action: string, ): Promise { try { - const stack = { name: stack_name }; + const stack = { id: stack_id }; const stackPath = await getStackPath(stack as Stack); return await command(stackPath); } catch (error: any) { throw new Error( - `Error while ${action} stack "${stack_name}": ${error.message || error}`, + `Error while ${action} stack "${stack_id}": ${error.message || error}`, ); } } @@ -54,6 +54,7 @@ export async function deployStack( const resolvedPrefix = stack_prefix ?? ""; const stack_config: stacks_config = { + id: 0, name: name, version: version, source, @@ -87,10 +88,10 @@ export async function deployStack( } } -export async function stopStack(stack_name: string): Promise { +export async function stopStack(stack_id: number): Promise { try { await runStackCommand( - stack_name, + stack_id, (cwd) => DockerCompose.downAll({ cwd }), "stopping", ); @@ -101,10 +102,10 @@ export async function stopStack(stack_name: string): Promise { } } -export async function startStack(stack_name: string): Promise { +export async function startStack(stack_id: number): Promise { try { await runStackCommand( - stack_name, + stack_id, (cwd) => DockerCompose.upAll({ cwd }), "starting", ); @@ -115,10 +116,10 @@ export async function startStack(stack_name: string): Promise { } } -export async function pullStackImages(stack_name: string): Promise { +export async function pullStackImages(stack_id: number): Promise { try { await runStackCommand( - stack_name, + stack_id, (cwd) => DockerCompose.pullAll({ cwd }), "pulling images for", ); @@ -129,10 +130,10 @@ export async function pullStackImages(stack_name: string): Promise { } } -export async function restartStack(stack_name: string): Promise { +export async function restartStack(stack_id: number): Promise { try { await runStackCommand( - stack_name, + stack_id, (cwd) => DockerCompose.restartAll({ cwd }), "restarting", ); @@ -143,10 +144,10 @@ export async function restartStack(stack_name: string): Promise { } } -export async function getStackStatus(stack_name: string): Promise { +export async function getStackStatus(stack_id: number): Promise { try { return await runStackCommand( - stack_name, + stack_id, async (cwd) => { const rawStatus = await DockerCompose.ps({ cwd }); return rawStatus.data.services.reduce((acc: any, service: any) => { @@ -163,6 +164,24 @@ export async function getStackStatus(stack_name: string): Promise { } } +export async function removeStack(stack_id: number): Promise { + try { + await runStackCommand( + stack_id, + async (cwd) => { + await DockerCompose.down({ cwd }); + }, + "removing", + ); + + dbFunctions.deleteStack(stack_id); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + export async function getAllStacksStatus(): Promise> { try { const stacks = dbFunctions.getStacks() as stacks_config[]; @@ -170,7 +189,7 @@ export async function getAllStacksStatus(): Promise> { const statusResults = await Promise.all( stacks.map(async (stack) => { const status = await runStackCommand( - stack.name, + stack.id, async (cwd) => { const rawStatus = await DockerCompose.ps({ cwd }); return rawStatus.data.services.reduce((acc: any, service: any) => { diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index dcf97a73..d3e88c75 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -2,6 +2,7 @@ import { Elysia, t } from "elysia"; import { dbFunctions } from "~/core/database/repository"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; +import { DockerHost } from "~/typings/docker"; export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) .post( @@ -9,13 +10,13 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) async ({ set, body }) => { try { set.headers["Content-Type"] = "application/json"; - dbFunctions.addDockerHost(body); + dbFunctions.addDockerHost(body as DockerHost); return responseHandler.ok(set, `Added docker host (${body.name})`); } catch (error: unknown) { return responseHandler.error( set, "Error adding docker Host", - error as string + error as string, ); } }, @@ -29,7 +30,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) hostAddress: t.String(), secure: t.Boolean(), }), - } + }, ) .post( @@ -37,12 +38,13 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) async ({ set, body }) => { try { set.status = 200; - return dbFunctions.updateDockerHost(body); + dbFunctions.updateDockerHost(body); + return responseHandler.ok(set, `Updated docker host (${body.id})`); } catch (error) { return responseHandler.error( set, error as string, - "Failed to update host" + "Failed to update host", ); } }, @@ -57,7 +59,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) hostAddress: t.String(), secure: t.Boolean(), }), - } + }, ) .get( @@ -72,7 +74,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) return responseHandler.error( set, error as string, - "Failed to retrieve hosts" + "Failed to retrieve hosts", ); } }, @@ -81,5 +83,31 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) tags: ["Management"], description: "Returns an Array of Host-config-objects", }, - } + }, + ) + + .delete( + "/hosts/:id", + async ({ set, params }) => { + try { + set.status = 200; + dbFunctions.deleteDockerHost(params.id); + return responseHandler.ok(set, `Deleted docker host (${params.id})`); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to delete host", + ); + } + }, + { + detail: { + tags: ["Management"], + description: "Delete an existing host", + }, + params: t.Object({ + id: t.Number(), + }), + }, ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index d212ba4c..41304da8 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -48,18 +48,18 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body.automatic_reboot_on_error, isCustom, image_updates, - body.stack_prefix + body.stack_prefix, ); logger.info(`Deployed Stack (${body.name})`); return responseHandler.ok( set, - `Stack ${body.name} deployed successfully` + `Stack ${body.name} deployed successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error deploying stack" + "Error deploying stack", ); } }, @@ -79,7 +79,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) source: t.String(), stack_prefix: t.Optional(t.String()), }), - } + }, ) .post( "/start", @@ -92,13 +92,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Started Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} started successfully` + `Stack ${body.stack} started successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error starting stack" + "Error starting stack", ); } }, @@ -107,7 +107,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - } + }, ) .post( "/stop", @@ -120,13 +120,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Stopped Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} stopped successfully` + `Stack ${body.stack} stopped successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error stopping stack" + "Error stopping stack", ); } }, @@ -135,7 +135,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - } + }, ) .post( "/restart", @@ -148,13 +148,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Restarted Stack (${body.stack})`); return responseHandler.ok( set, - `Stack ${body.stack} restarted successfully` + `Stack ${body.stack} restarted successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error restarting stack" + "Error restarting stack", ); } }, @@ -163,7 +163,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - } + }, ) .post( "/pull-images", @@ -176,13 +176,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Pulled Stack images (${body.stack})`); return responseHandler.ok( set, - `Images for stack ${body.stack} pulled successfully` + `Images for stack ${body.stack} pulled successfully`, ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error pulling images" + "Error pulling images", ); } }, @@ -194,7 +194,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stack: t.Any(), }), - } + }, ) .get( "/status", @@ -206,7 +206,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) status = await getStackStatus(query.stack_name); res = responseHandler.ok( set, - `Stack ${query.stack_name} status retrieved successfully` + `Stack ${query.stack_name} status retrieved successfully`, ); logger.info("Fetched Stack status"); } else { @@ -219,7 +219,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stack status" + "Error getting stack status", ); } }, @@ -232,7 +232,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) query: t.Object({ stack_name: t.Any(), }), - } + }, ) .get( "/", @@ -245,7 +245,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stacks" + "Error getting stacks", ); } }, @@ -254,5 +254,5 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Returns an Array of Stack-config-objects", }, - } + }, ); diff --git a/src/typings/database.ts b/src/typings/database.ts index cebda604..96319e96 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -5,6 +5,7 @@ interface config { } interface stacks_config { + id: number; name: string; version: number; custom: boolean; diff --git a/src/typings/docker-compose.ts b/src/typings/docker-compose.ts index 9b23c2e4..9067abaa 100644 --- a/src/typings/docker-compose.ts +++ b/src/typings/docker-compose.ts @@ -3,6 +3,7 @@ export interface Stack { name: string; version: number; source: string; + id?: number; } export interface ComposeSpec { diff --git a/src/typings/docker.ts b/src/typings/docker.ts index e6701f6c..0d759bac 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -2,7 +2,7 @@ interface DockerHost { name: string; hostAddress: string; secure: boolean | number; - id?: number; + id: number; } interface ContainerInfo { @@ -17,7 +17,8 @@ interface ContainerInfo { } interface HostStats { - hostId: string; + hostName: string; + hostId: number; dockerVersion: string; apiVersion: string; os: string; From ac0b4309b9d9d5a1977177186183943972ae99da Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 29 Mar 2025 23:33:11 +0100 Subject: [PATCH 226/369] Fix: Refactor and Stacks now have IDs ! --- src/core/database/config.ts | 55 ++++ src/core/database/containerStats.ts | 34 +++ src/core/database/database.ts | 92 +++++++ src/core/database/dockerHosts.ts | 57 ++++ src/core/database/helper.ts | 13 +- src/core/database/hostStats.ts | 45 +++ src/core/database/index.ts | 19 ++ src/core/database/logs.ts | 73 +++++ src/core/database/stacks.ts | 72 +++++ src/core/docker/monitor.ts | 10 +- src/core/docker/scheduler.ts | 2 +- src/core/docker/store-container-stats.ts | 24 +- src/core/docker/store-host-stats.ts | 2 +- src/core/plugins/loader.ts | 23 +- src/core/stacks/controller.ts | 40 ++- .../trpc/procedures/api-config.procedure.ts | 2 +- .../procedures/docker-manager.procedure.ts | 2 +- .../trpc/procedures/docker-stats.procedure.ts | 14 +- src/core/trpc/procedures/logs.procedure.ts | 2 +- src/core/trpc/procedures/stacks.procedure.ts | 2 +- src/core/utils/logger.ts | 259 +++++++++++------- src/index.ts | 49 ++-- src/middleware/auth.ts | 8 +- src/routes/api-config.ts | 20 +- src/routes/docker-manager.ts | 2 +- src/routes/docker-stats.ts | 26 +- src/routes/docker-websocket.ts | 2 +- src/routes/logs.ts | 2 +- src/routes/stacks.ts | 80 ++++-- src/tests/cleanup.ts | 2 +- 30 files changed, 819 insertions(+), 214 deletions(-) create mode 100644 src/core/database/config.ts create mode 100644 src/core/database/containerStats.ts create mode 100644 src/core/database/database.ts create mode 100644 src/core/database/dockerHosts.ts create mode 100644 src/core/database/hostStats.ts create mode 100644 src/core/database/index.ts create mode 100644 src/core/database/logs.ts create mode 100644 src/core/database/stacks.ts diff --git a/src/core/database/config.ts b/src/core/database/config.ts new file mode 100644 index 00000000..126682e5 --- /dev/null +++ b/src/core/database/config.ts @@ -0,0 +1,55 @@ +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const stmt = { + update: db.prepare( + `UPDATE config SET fetching_interval = ?, keep_data_for = ?, api_key = ?`, + ), + select: db.prepare( + `SELECT keep_data_for, fetching_interval, api_key FROM config`, + ), + deleteOld: db.prepare( + `DELETE FROM container_stats WHERE timestamp < datetime('now', '-' || ? || ' days')`, + ), + deleteOldLogs: db.prepare( + `DELETE FROM backend_log_entries WHERE timestamp < datetime('now', '-' || ? || ' days')`, + ), +}; + +export function updateConfig( + fetching_interval: number, + keep_data_for: number, + api_key: string, +) { + return executeDbOperation( + "Update Config", + () => stmt.update.run(fetching_interval, keep_data_for, api_key), + () => { + if ( + typeof fetching_interval !== "number" || + typeof keep_data_for !== "number" + ) { + throw new TypeError("Invalid config parameters"); + } + }, + ); +} + +export function getConfig() { + return executeDbOperation("Get Config", () => stmt.select.all()); +} + +export function deleteOldData(days: number) { + return executeDbOperation( + "Delete Old Data", + () => { + db.transaction(() => { + stmt.deleteOld.run(days); + stmt.deleteOldLogs.run(days); + })(); + }, + () => { + if (typeof days !== "number") throw new TypeError("Invalid days type"); + }, + ); +} diff --git a/src/core/database/containerStats.ts b/src/core/database/containerStats.ts new file mode 100644 index 00000000..d0fb1970 --- /dev/null +++ b/src/core/database/containerStats.ts @@ -0,0 +1,34 @@ +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const stmt = db.prepare(` + INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) +`); + +export function addContainerStats( + id: string, + hostId: string, + name: string, + image: string, + status: string, + state: string, + cpu_usage: number, + memory_usage: number, +) { + return executeDbOperation( + "Add Container Stats", + () => + stmt.run(id, hostId, name, image, status, state, cpu_usage, memory_usage), + () => { + if ( + typeof id !== "string" || + typeof hostId !== "string" || + typeof cpu_usage !== "number" || + typeof memory_usage !== "number" + ) { + throw new TypeError("Invalid container stats parameters"); + } + }, + ); +} diff --git a/src/core/database/database.ts b/src/core/database/database.ts new file mode 100644 index 00000000..5173e448 --- /dev/null +++ b/src/core/database/database.ts @@ -0,0 +1,92 @@ +import { Database } from "bun:sqlite"; + +export const db = new Database("dockstatapi.db", { strict: true }); +db.exec("PRAGMA journal_mode = WAL;"); + +export function init() { + db.exec(` + CREATE TABLE IF NOT EXISTS backend_log_entries ( + timestamp STRING NOT NULL, + level TEXT NOT NULL, + message TEXT NOT NULL, + file TEXT NOT NULL, + line NUMBER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS stacks_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + version INTEGER NOT NULL, + custom BOOLEAN NOT NULL, + source TEXT NOT NULL, + container_count INTEGER NOT NULL, + stack_prefix TEXT NOT NULL, + automatic_reboot_on_error BOOLEAN NOT NULL, + image_updates BOOLEAN NOT NULL + ); + + CREATE TABLE IF NOT EXISTS docker_hosts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + hostAddress TEXT NOT NULL, + secure BOOLEAN NOT NULL + ); + + CREATE TABLE IF NOT EXISTS host_stats ( + hostId INTEGER PRIMARY KEY NOT NULL, + hostName TEXT NOT NULL, + dockerVersion TEXT NOT NULL, + apiVersion TEXT NOT NULL, + os TEXT NOT NULL, + architecture TEXT NOT NULL, + totalMemory INTEGER NOT NULL, + totalCPU INTEGER NOT NULL, + labels TEXT NOT NULL, + containers INTEGER NOT NULL, + containersRunning INTEGER NOT NULL, + containersStopped INTEGER NOT NULL, + containersPaused INTEGER NOT NULL, + images INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS container_stats ( + id TEXT NOT NULL, + hostId TEXT NOT NULL, + name TEXT NOT NULL, + image TEXT NOT NULL, + status TEXT NOT NULL, + state TEXT NOT NULL, + cpu_usage FLOAT NOT NULL, + memory_usage FLOAT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS config ( + keep_data_for NUMBER NOT NULL, + fetching_interval NUMBER NOT NULL, + api_key TEXT NOT NULL + ); + `); + + const configRow = db + .prepare(`SELECT COUNT(*) AS count FROM config`) + .get() as { count: number }; + + if (configRow.count === 0) { + db.prepare( + `INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")`, + ).run(); + } + + const hostRow = db + .prepare(`SELECT COUNT(*) AS count FROM docker_hosts`) + .get() as { count: number }; + + if (hostRow.count === 0) { + db.prepare( + `INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)`, + ).run("Localhost", "localhost:2375", false); + } +} + +init(); diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts new file mode 100644 index 00000000..bdaf2d1a --- /dev/null +++ b/src/core/database/dockerHosts.ts @@ -0,0 +1,57 @@ +import { db } from "./database"; +import { executeDbOperation } from "./helper"; +import type { DockerHost } from "~/typings/docker"; + +const stmt = { + insert: db.prepare( + `INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)`, + ), + selectAll: db.prepare( + `SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC`, + ), + update: db.prepare( + `UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?`, + ), + delete: db.prepare(`DELETE FROM docker_hosts WHERE id = ?`), +}; + +export function addDockerHost(host: DockerHost) { + return executeDbOperation( + "Add Docker Host", + () => stmt.insert.run(host.name, host.hostAddress, host.secure), + () => { + if (!host.name || !host.hostAddress) + throw new Error("Missing required fields"); + if (typeof host.secure !== "boolean") + throw new TypeError("Invalid secure type"); + }, + ); +} + +export function getDockerHosts(): DockerHost[] { + return executeDbOperation( + "Get Docker Hosts", + () => stmt.selectAll.all() as DockerHost[], + ); +} + +export function updateDockerHost(host: DockerHost) { + return executeDbOperation( + "Update Docker Host", + () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), + () => { + if (!host.id || typeof host.id !== "number") + throw new Error("Invalid host ID"); + }, + ); +} + +export function deleteDockerHost(id: number) { + return executeDbOperation( + "Delete Docker Host", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid ID type"); + }, + ); +} diff --git a/src/core/database/helper.ts b/src/core/database/helper.ts index 753bc892..3edbdaa8 100644 --- a/src/core/database/helper.ts +++ b/src/core/database/helper.ts @@ -1,17 +1,22 @@ -import { logger } from "../utils/logger"; +import { logger } from "~/core/utils/logger"; export function executeDbOperation( label: string, operation: () => T, - validate?: () => void + validate?: () => void, + dontLog?: boolean, ): T { const startTime = Date.now(); - logger.debug(`__task__ __db__ ${label} ⏳`); + if (dontLog !== true) { + logger.debug(`__task__ __db__ ${label} ⏳`); + } if (validate) { validate(); } const result = operation(); const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); + if (dontLog !== true) { + logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); + } return result; } diff --git a/src/core/database/hostStats.ts b/src/core/database/hostStats.ts new file mode 100644 index 00000000..04aa426e --- /dev/null +++ b/src/core/database/hostStats.ts @@ -0,0 +1,45 @@ +import { db } from "./database"; +import { executeDbOperation } from "./helper"; +import type { HostStats } from "~/typings/docker"; + +const stmt = db.prepare(` + INSERT INTO host_stats ( + hostId, hostName, dockerVersion, apiVersion, os, architecture, + totalMemory, totalCPU, labels, containers, containersRunning, + containersStopped, containersPaused, images + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(hostId) DO UPDATE SET + dockerVersion = excluded.dockerVersion, + apiVersion = excluded.apiVersion, + os = excluded.os, + architecture = excluded.architecture, + totalMemory = excluded.totalMemory, + totalCPU = excluded.totalCPU, + labels = excluded.labels, + containers = excluded.containers, + containersRunning = excluded.containersRunning, + containersStopped = excluded.containersStopped, + containersPaused = excluded.containersPaused, + images = excluded.images +`); + +export function updateHostStats(stats: HostStats) { + return executeDbOperation("Update Host Stats", () => + stmt.run( + stats.hostId, + stats.hostName, + stats.dockerVersion, + stats.apiVersion, + stats.os, + stats.architecture, + stats.totalMemory, + stats.totalCPU, + JSON.stringify(stats.labels), + stats.containers, + stats.containersRunning, + stats.containersStopped, + stats.containersPaused, + stats.images, + ), + ); +} diff --git a/src/core/database/index.ts b/src/core/database/index.ts new file mode 100644 index 00000000..3559fe94 --- /dev/null +++ b/src/core/database/index.ts @@ -0,0 +1,19 @@ +import { init } from "~/core/database/database"; + +init(); + +import * as dockerHosts from "~/core/database/dockerHosts"; +import * as logs from "~/core/database/logs"; +import * as config from "~/core/database/config"; +import * as containerStats from "~/core/database/containerStats"; +import * as hostStats from "~/core/database/hostStats"; +import * as stacks from "~/core/database/stacks"; + +export const dbFunctions = { + ...dockerHosts, + ...logs, + ...config, + ...containerStats, + ...hostStats, + ...stacks, +}; diff --git a/src/core/database/logs.ts b/src/core/database/logs.ts new file mode 100644 index 00000000..26ce2c1c --- /dev/null +++ b/src/core/database/logs.ts @@ -0,0 +1,73 @@ +import { logStreamData } from "~/typings/websocket"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const stmt = { + insert: db.prepare( + `INSERT INTO backend_log_entries (timestamp, level, message, file, line) VALUES (?, ?, ?, ?, ?)`, + ), + selectAll: db.prepare( + `SELECT timestamp, level, message, file, line FROM backend_log_entries ORDER BY timestamp DESC`, + ), + selectByLevel: db.prepare( + `SELECT timestamp, level, message, file, line FROM backend_log_entries WHERE level = ?`, + ), + deleteAll: db.prepare(`DELETE FROM backend_log_entries`), + deleteByLevel: db.prepare(`DELETE FROM backend_log_entries WHERE level = ?`), +}; + +export function addLogEntry(data: logStreamData) { + return executeDbOperation( + "Add Log Entry", + () => + stmt.insert.run( + data.level, + data.timestamp, + data.message, + data.file, + data.line, + ), + () => { + if ( + typeof data.level !== "string" || + typeof data.timestamp !== "string" || + typeof data.message !== "string" || + typeof data.file !== "string" || + typeof data.line !== "number" + ) { + throw new TypeError( + `Invalid log entry parameters ${data.file} ${data.line} ${data.message} ${data}`, + ); + } + }, + true, + ); +} + +export function getAllLogs() { + return executeDbOperation("Get All Logs", () => stmt.selectAll.all()); +} + +export function getLogsByLevel(level: string) { + return executeDbOperation( + "Get Logs By Level", + () => stmt.selectByLevel.all(level), + () => { + if (typeof level !== "string") throw new TypeError("Invalid level type"); + }, + ); +} + +export function clearAllLogs() { + return executeDbOperation("Clear All Logs", () => stmt.deleteAll.run()); +} + +export function clearLogsByLevel(level: string) { + return executeDbOperation( + "Clear Logs By Level", + () => stmt.deleteByLevel.run(level), + () => { + if (typeof level !== "string") throw new TypeError("Invalid level type"); + }, + ); +} diff --git a/src/core/database/stacks.ts b/src/core/database/stacks.ts new file mode 100644 index 00000000..04916252 --- /dev/null +++ b/src/core/database/stacks.ts @@ -0,0 +1,72 @@ +import { Stack } from "~/typings/docker-compose"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; +import type { stacks_config } from "~/typings/database"; + +const stmt = { + insert: db.prepare(` + INSERT INTO stacks_config ( + name, version, custom, source, container_count, + stack_prefix, automatic_reboot_on_error, image_updates + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `), + selectAll: db.prepare(` + SELECT id, name, version, custom, source, container_count, stack_prefix, + automatic_reboot_on_error, image_updates + FROM stacks_config + ORDER BY name DESC + `), + update: db.prepare(` + UPDATE stacks_config SET + version = ?, custom = ?, source = ?, container_count = ?, + stack_prefix = ?, automatic_reboot_on_error = ?, image_updates = ? + WHERE name = ? + `), + delete: db.prepare(`DELETE FROM stacks_config WHERE id = ?`), +}; + +export function addStack(stack: stacks_config) { + return executeDbOperation("Add Stack", () => + stmt.insert.run( + stack.name, + stack.version, + stack.custom, + stack.source, + stack.container_count, + stack.stack_prefix, + stack.automatic_reboot_on_error, + stack.image_updates, + ), + ); +} + +export function getStacks() { + return executeDbOperation("Get Stacks", () => + stmt.selectAll.all(), + ) as Stack[]; +} + +export function deleteStack(id: number) { + return executeDbOperation( + "Delete Stack", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid stack ID"); + }, + ); +} + +export function updateStack(stack: stacks_config) { + return executeDbOperation("Update Stack", () => + stmt.update.run( + stack.version, + stack.custom, + stack.source, + stack.container_count, + stack.stack_prefix, + stack.automatic_reboot_on_error, + stack.image_updates, + stack.name, + ), + ); +} diff --git a/src/core/docker/monitor.ts b/src/core/docker/monitor.ts index 1257326c..eadd5f62 100644 --- a/src/core/docker/monitor.ts +++ b/src/core/docker/monitor.ts @@ -1,5 +1,5 @@ import type { DockerHost } from "~/typings/docker"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { logger } from "~/core/utils/logger"; import { pluginManager } from "../plugins/plugin-manager"; @@ -12,7 +12,7 @@ export async function monitorDockerEvents() { try { hosts = dbFunctions.getDockerHosts(); logger.debug( - `Retrieved ${hosts.length} Docker host(s) for event monitoring.` + `Retrieved ${hosts.length} Docker host(s) for event monitoring.`, ); } catch (error: unknown) { logger.error(`Error retrieving Docker hosts: ${(error as Error).message}`); @@ -58,7 +58,7 @@ async function startFor(host: DockerHost) { event = JSON.parse(line); } catch (parseErr: any) { logger.error( - `Failed to parse event from host ${host.name}: ${parseErr.message}` + `Failed to parse event from host ${host.name}: ${parseErr.message}`, ); continue; } @@ -113,7 +113,7 @@ async function startFor(host: DockerHost) { break; default: logger.debug( - `Unhandled container event "${action}" on host ${host.name}` + `Unhandled container event "${action}" on host ${host.name}`, ); } } @@ -132,7 +132,7 @@ async function startFor(host: DockerHost) { }); } catch (streamErr: any) { logger.error( - `Failed to start events stream for host ${host.name}: ${streamErr.message}` + `Failed to start events stream for host ${host.name}: ${streamErr.message}`, ); } } diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index e4adf9ce..2d7fae1e 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -1,5 +1,5 @@ import storeContainerData from "~/core/docker/store-container-stats"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { config } from "~/typings/database"; import { logger } from "~/core/utils/logger"; import storeHostData from "~/core/docker//store-host-stats"; diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts index 6bcc1684..69c12e03 100644 --- a/src/core/docker/store-container-stats.ts +++ b/src/core/docker/store-container-stats.ts @@ -1,5 +1,5 @@ import { getDockerClient } from "~/core/docker/client"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import Docker from "dockerode"; import { calculateCpuPercent, @@ -23,7 +23,7 @@ async function storeContainerData() { } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); throw new Error( - `Failed to ping docker host "${host.name}": ${errMsg}` + `Failed to ping docker host "${host.name}": ${errMsg}`, ); } @@ -33,7 +33,7 @@ async function storeContainerData() { } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); throw new Error( - `Failed to list containers on host "${host.name}": ${errMsg}` + `Failed to list containers on host "${host.name}": ${errMsg}`, ); } @@ -52,20 +52,20 @@ async function storeContainerData() { error instanceof Error ? error.message : String(error); return reject( new Error( - `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}` - ) + `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, + ), ); } if (!stats) { return reject( new Error( - `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".` - ) + `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".`, + ), ); } resolve(stats); }); - } + }, ); dbFunctions.addContainerStats( @@ -76,18 +76,18 @@ async function storeContainerData() { containerInfo.Status, containerInfo.State, calculateCpuPercent(stats), - calculateMemoryUsage(stats) + calculateMemoryUsage(stats), ); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); throw new Error( - `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}` + `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, ); } - }) + }), ); - }) + }), ); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); diff --git a/src/core/docker/store-host-stats.ts b/src/core/docker/store-host-stats.ts index a7fe6e15..9536bc14 100644 --- a/src/core/docker/store-host-stats.ts +++ b/src/core/docker/store-host-stats.ts @@ -1,5 +1,5 @@ import { logger } from "~/core/utils/logger"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { DockerHost, HostStats } from "~/typings/docker"; import { getDockerClient } from "~/core/docker/client"; import { DockerInfo } from "~/typings/dockerode"; diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts index db77bedd..6fad398c 100644 --- a/src/core/plugins/loader.ts +++ b/src/core/plugins/loader.ts @@ -8,19 +8,34 @@ export async function loadPlugins(pluginDir: string) { const pluginPath = path.join(process.cwd(), pluginDir); logger.debug(`Loading plugins (${pluginPath})`); + if (!fs.existsSync(pluginPath)) { - return; + throw new Error(`Failed to check plugin directory`); } + logger.debug(`Plugin directory exists`); let pluginCount = 0; - const files = fs.readdirSync(pluginPath); + let files; + try { + files = fs.readdirSync(pluginPath); + logger.debug(`Found ${files.length} files in plugin directory`); + } catch (error) { + throw new Error(`Failed to read plugin-directory: ${error}`); + } + + if (!files) { + logger.info(`No plugins found in ${pluginPath}`); + return; + } for (const file of files) { if (!file.endsWith(".plugin.ts")) { - continue - }; + logger.debug(`Skipping non-plugin file: ${file}`); + continue; + } const absolutePath = path.join(pluginPath, file); + logger.info(`Loading plugin: ${absolutePath}`); try { await checkFileForChangeMe(absolutePath); const module = await import(absolutePath); diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 1f2e0341..7f1aeb9d 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -1,9 +1,21 @@ -import { dbFunctions } from "../database/repository"; +import { dbFunctions } from "~/core/database"; import YAML from "yaml"; -import { logger } from "../utils/logger"; +import { logger } from "~/core/utils/logger"; import DockerCompose from "docker-compose"; import type { Stack, ComposeSpec } from "~/typings/docker-compose"; import type { stacks_config } from "~/typings/database"; +import { rm } from "node:fs/promises"; +import { ErrorLike } from "bun"; + +async function getStackName(stack_id: number): Promise { + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = stacks.find((stack) => Number(stack.id) === Number(stack_id)); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; +} async function runStackCommand( stack_id: number, @@ -11,7 +23,7 @@ async function runStackCommand( action: string, ): Promise { try { - const stack = { id: stack_id }; + const stack = { id: stack_id, name: await getStackName(stack_id) }; const stackPath = await getStackPath(stack as Stack); return await command(stackPath); } catch (error: any) { @@ -174,7 +186,25 @@ export async function removeStack(stack_id: number): Promise { "removing", ); + const stackName = await getStackName(stack_id); + + const stack = { + id: stack_id, + }; + + const stackPath = await getStackPath(stack as Stack); + + try { + await rm("stackPath", { recursive: true }); + } catch (error: any) { + if (error.code === "ENOENT") { + console.log("Directory doesn't exist"); + } else { + throw error; + } + } dbFunctions.deleteStack(stack_id); + logger.info(`Stack ${stackName} (${stack_id}) removed successfully`); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error(errorMsg); @@ -184,12 +214,12 @@ export async function removeStack(stack_id: number): Promise { export async function getAllStacksStatus(): Promise> { try { - const stacks = dbFunctions.getStacks() as stacks_config[]; + const stacks = dbFunctions.getStacks(); const statusResults = await Promise.all( stacks.map(async (stack) => { const status = await runStackCommand( - stack.id, + stack.id as number, async (cwd) => { const rawStatus = await DockerCompose.ps({ cwd }); return rawStatus.data.services.reduce((acc: any, service: any) => { diff --git a/src/core/trpc/procedures/api-config.procedure.ts b/src/core/trpc/procedures/api-config.procedure.ts index 6b3b2488..f5175684 100644 --- a/src/core/trpc/procedures/api-config.procedure.ts +++ b/src/core/trpc/procedures/api-config.procedure.ts @@ -1,4 +1,4 @@ -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { version, diff --git a/src/core/trpc/procedures/docker-manager.procedure.ts b/src/core/trpc/procedures/docker-manager.procedure.ts index 7621a5a4..a91e7691 100644 --- a/src/core/trpc/procedures/docker-manager.procedure.ts +++ b/src/core/trpc/procedures/docker-manager.procedure.ts @@ -1,4 +1,4 @@ -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; diff --git a/src/core/trpc/procedures/docker-stats.procedure.ts b/src/core/trpc/procedures/docker-stats.procedure.ts index 017e880a..0a289293 100644 --- a/src/core/trpc/procedures/docker-stats.procedure.ts +++ b/src/core/trpc/procedures/docker-stats.procedure.ts @@ -1,5 +1,5 @@ import Docker from "dockerode"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { calculateCpuPercent, @@ -47,7 +47,7 @@ export const dockerStatsProcedure = router({ code: "INTERNAL_SERVER_ERROR", message: "Error fetching container stats", cause: error, - }) + }), ); } if (!stats) { @@ -55,12 +55,12 @@ export const dockerStatsProcedure = router({ new TRPCError({ code: "NOT_FOUND", message: "No stats available", - }) + }), ); } resolve(stats as Docker.ContainerStats); }); - } + }, ); containers.push({ @@ -76,16 +76,16 @@ export const dockerStatsProcedure = router({ } catch (containerError) { logger.error( "Error fetching container stats", - containerError + containerError, ); } - }) + }), ); logger.debug(`Fetched stats for ${host.name}`); } catch (hostError) { logger.error("Error fetching containers for host", hostError); } - }) + }), ); logger.debug("Fetched all containers across all hosts"); diff --git a/src/core/trpc/procedures/logs.procedure.ts b/src/core/trpc/procedures/logs.procedure.ts index 520a2cb6..b15fc9f7 100644 --- a/src/core/trpc/procedures/logs.procedure.ts +++ b/src/core/trpc/procedures/logs.procedure.ts @@ -1,4 +1,4 @@ -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; diff --git a/src/core/trpc/procedures/stacks.procedure.ts b/src/core/trpc/procedures/stacks.procedure.ts index db7a85c1..495fd32d 100644 --- a/src/core/trpc/procedures/stacks.procedure.ts +++ b/src/core/trpc/procedures/stacks.procedure.ts @@ -1,4 +1,4 @@ -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index fce5909a..6c0c5eea 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -1,135 +1,200 @@ import { createLogger, format, transports } from "winston"; +import type { TransformableInfo } from "logform"; import path from "path"; import chalk, { ChalkInstance } from "chalk"; -import { dbFunctions } from "../database/repository"; +import { dbFunctions } from "~/core/database"; import wrapAnsi from "wrap-ansi"; import { logToClients } from "~/routes/live-logs"; -import { logStreamData } from "~/typings/websocket"; +import type { logStreamData } from "~/typings/websocket"; + +const padNewlines = process.env.PAD_NEW_LINES !== "false"; + +type LogLevel = + | "error" + | "warn" + | "info" + | "debug" + | "verbose" + | "silly" + | "task" + | "ut"; + +interface CustomTransformableInfo extends TransformableInfo { + file: string; + line: number; +} + +type LogStreamData = Omit & { + message: string; +}; const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; -// Change to false here if dont want the spacing on a wrapped line -const padNewlines: boolean = process.env.PAD_NEW_LINES === "true" || true; - -const fileLineFormat = format((info) => { +const formatTerminalMessage = (message: string, prefix: string): string => { try { - const stack = new Error().stack?.split("\n"); - if (stack) { - for (let i = 2; i < stack.length; i++) { - const line = stack[i].trim(); - // Exclude lines from node_modules or the current file - if ( - !line.includes("node_modules") && - !line.includes(path.basename(__filename)) - ) { - const matches = line.match(/\(?(.+):(\d+):(\d+)\)?$/); - if (matches) { - info.file = path.basename(matches[1]); - info.line = parseInt(matches[2], 10); - break; - } - } - } - } - } catch (err) { - // Ignore errors during stack trace extraction - } - return info; -}); + const cleanPrefix = prefix.replace(ansiRegex, ""); + const maxWidth = process.stdout.columns || 80; + const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); -const formatTerminalMessage = (message: string, prefixLength: number) => { - const maxWidth = process.stdout.columns || 80; - const wrapWidth = maxWidth - prefixLength - 15; + if (!padNewlines) return message; - if (padNewlines) { - const wrapped = wrapAnsi(chalk.gray(message), wrapWidth, { + const wrapped = wrapAnsi(message, wrapWidth, { trim: true, hard: true, + wordWrap: true, }); return wrapped .split("\n") - .map((line, i) => (i === 0 ? line : " ".repeat(prefixLength) + line)) + .map((line, index) => { + return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; + }) .join("\n"); + } catch (error) { + console.error("Error formatting terminal message:", error); + return message; + } +}; + +const levelColors: Record = { + error: chalk.red.bold, + warn: chalk.yellow.bold, + info: chalk.green.bold, + debug: chalk.blue.bold, + verbose: chalk.cyan.bold, + silly: chalk.magenta.bold, + task: chalk.cyan.bold, + ut: chalk.hex("#9D00FF"), +}; + +const handleWebSocketLog = ( + level: string, + timestamp: string, + message: string, + file: string, + line: number, +) => { + try { + const data = { + timestamp, + level: level, + message: message, + file: file, + line: line, + }; + + logToClients(data); + } catch (error) { + console.error( + `WebSocket logging failed: ${error instanceof Error ? error.message : error}`, + ); + } +}; + +const handleDatabaseLog = ( + level: string, + timestamp: string, + message: string, + file: string, + line: number, +): void => { + try { + const data = { + timestamp, + level, + message, + file: file, + line: line, + }; + + dbFunctions.addLogEntry(data); + } catch (error) { + console.error( + `Database logging failed: ${error instanceof Error ? error.message : error}`, + ); } - return message; }; +// Main logger export const logger = createLogger({ level: process.env.LOG_LEVEL || "debug", format: format.combine( format.timestamp({ format: "DD/MM HH:mm:ss" }), - fileLineFormat(), - format.printf(({ timestamp, level, message, file, line }) => { - const levelColors: Record = { - error: chalk.red.bold, - warn: chalk.yellow.bold, - info: chalk.green.bold, - debug: chalk.blue.bold, - verbose: chalk.cyan.bold, - silly: chalk.magenta.bold, - task: chalk.cyan.bold, - ut: chalk.hex("#9D00FF"), - }; - - if ((message as string).startsWith("__task__")) { - message = (message as string).replaceAll("__task__", "").trimStart(); - level = "task"; - if ((message as string).startsWith("__db__")) { - message = (message as string).replaceAll("__db__", "").trimStart(); - message = `${chalk.magenta("DB")} ${message}`; + format((info) => { + const stack = new Error().stack?.split("\n"); + let file = "unknown"; + let line = 0; + + if (stack) { + for (let i = 2; i < stack.length; i++) { + const lineStr = stack[i].trim(); + if ( + !lineStr.includes("node_modules") && + !lineStr.includes(path.basename(__filename)) + ) { + const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); + if (matches) { + file = path.basename(matches[1]); + line = parseInt(matches[2], 10); + break; + } + } } } - - if ((message as string).startsWith("__UT__")) { - message = (message as string).replaceAll("__UT__", "").trimStart(); - level = "ut"; + return { ...info, file, line }; + })(), + format.printf((info) => { + const { timestamp, level, message, file, line } = + info as CustomTransformableInfo; + let processedLevel = level as LogLevel; + let processedMessage = String(message); + + if (processedMessage.startsWith("__task__")) { + processedMessage = processedMessage + .replace(/__task__/g, "") + .trimStart(); + processedLevel = "task"; + if (processedMessage.startsWith("__db__")) { + processedMessage = processedMessage + .replace(/__db__/g, "") + .trimStart(); + processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; + } + } else if (processedMessage.startsWith("__UT__")) { + processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); + processedLevel = "ut"; } - if ((file as string).includes("plugin.ts")) { - message = `[ ${chalk.greenBright("Plugin")} ] ${message}`; + if (file.endsWith("plugin.ts")) { + processedMessage = `[ ${chalk.greenBright("Plugin")} ] ${processedMessage}`; } - const logStreamData: logStreamData = { - timestamp: timestamp as string, - level: level as string, - message: (message as string).replace(ansiRegex, ""), - file: file as string, - line: line as number, - }; - - logToClients(logStreamData); - - const paddedLevel = level.toUpperCase().padEnd(5); - const coloredLevel = (levelColors[level] || chalk.white)(paddedLevel); - const coloredContext = chalk.cyan(`${file as string}:${line as number}`); + const paddedLevel = processedLevel.toUpperCase().padEnd(5); + const coloredLevel = (levelColors[processedLevel] || chalk.white)( + paddedLevel, + ); + const coloredContext = chalk.cyan(`${file}:${line}`); const coloredTimestamp = chalk.yellow(timestamp); - if (process.env.NODE_ENV !== "dev") { - return `${coloredLevel} [ ${coloredTimestamp} ] - ${chalk.gray( - message, - )} - [ ${coloredContext} ]`; - } - const prefix = `${paddedLevel} [ ${timestamp} ] - `; - const prefixLength = prefix.length; - const formattedMessage = formatTerminalMessage( - message as string, - prefixLength + const formattedMessage = padNewlines + ? formatTerminalMessage(processedMessage, prefix) + : processedMessage; + + handleDatabaseLog( + coloredTimestamp.replace(ansiRegex, "").trim(), + coloredLevel.replace(ansiRegex, "").trim(), + processedMessage.replace(ansiRegex, "").trim(), + file.trim(), + line, + ); + handleWebSocketLog( + coloredLevel.replace(ansiRegex, "").trim(), + coloredTimestamp.replace(ansiRegex, "").trim(), + processedMessage.replace(ansiRegex, "").trim(), + file.trim(), + line, ); - - try { - dbFunctions.addLogEntry( - (level as string).replace(ansiRegex, ""), - (message as string).replace(ansiRegex, ""), - (file as string).replace(ansiRegex, ""), - line as number, - ); - } catch (error) { - // Use console.error to avoid recursive logging - console.error(`Error inserting log into DB: ${String(error)}`); - process.abort(); - } return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; }), diff --git a/src/index.ts b/src/index.ts index f96da4b5..c7d7f11d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ +import { dbFunctions } from "~/core/database"; import { swagger } from "@elysiajs/swagger"; import { Elysia } from "elysia"; -import { dbFunctions } from "~/core/database/repository"; import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { dockerRoutes } from "~/routes/docker-manager"; @@ -20,7 +20,8 @@ import { liveLogs } from "./routes/live-logs"; import { utilRoutes } from "./routes/utils"; console.log(""); -dbFunctions.init(); + +logger.info("Starting DockStatAPI"); export const DockStatAPI = new Elysia() .use(staticPlugin()) @@ -68,7 +69,7 @@ export const DockStatAPI = new Elysia() }, ], }, - }) + }), ) .onBeforeHandle(async (context) => { const { path, request, set } = context; @@ -108,8 +109,17 @@ export const DockStatAPI = new Elysia() async function startServer() { try { - await loadPlugins("./src/plugins"); - await setSchedules(); + try { + await loadPlugins("./src/plugins"); + } catch (error) { + throw new Error(`Failed to load plugins: ${error}`); + } + + try { + await setSchedules(); + } catch (error) { + throw new Error(`Failed to set schedules: ${error}`); + } monitorDockerEvents().catch((error) => { logger.error(`Monitoring Error: ${error}`); @@ -120,22 +130,27 @@ async function startServer() { if (apiKey === "changeme") { logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", ); } - DockStatAPI.listen(3000, ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger` - ); - logger.info( - `tRPC Endpoint available at: http://${hostname}:${port}/trpc` - ); - }); + try { + DockStatAPI.listen(3000, ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + ); + logger.info( + `tRPC Endpoint available at: http://${hostname}:${port}/trpc`, + ); + }); + } catch (error) { + logger.error("Failed to start server:", error); + process.exit(1); + } } catch (error) { - logger.error("Failed to start server:", error); + logger.error("Error while starting server:", error); process.exit(1); } } diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 2672f88a..17ae2c6b 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -1,4 +1,4 @@ -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { config } from "~/typings/database"; import { set } from "~/typings/elysiajs"; @@ -16,7 +16,7 @@ export async function hashApiKey(apiKey: string): Promise { async function validateApiKeyHash( providedKey: string, - storedHash: string + storedHash: string, ): Promise { logger.debug("Validating API key hash"); try { @@ -30,7 +30,7 @@ async function validateApiKeyHash( } async function getApiKeyFromDb( - apiKey: string + apiKey: string, ): Promise<{ hash: string } | null> { const dbApiKey = (dbFunctions.getConfig() as config[])[0].api_key; logger.debug(`Querying database for API key: ${apiKey}`); @@ -44,7 +44,7 @@ export async function validateApiKey(request: Request, set: set) { if (process.env.NODE_ENV != "production") { logger.warn( - "API Key validation deactivated, since running in development mode" + "API Key validation deactivated, since running in development mode", ); return { apiKey }; } else if (!apiKey) { diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 17ea3521..2059b0e2 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -1,5 +1,5 @@ import { Elysia, t } from "elysia"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; import { config } from "~/typings/database"; @@ -32,7 +32,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error getting the DockStatAPI config" + "Error getting the DockStatAPI config", ); } }, @@ -41,7 +41,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Returns DockStatAPI's config", }, - } + }, ) .get( "/plugins", @@ -52,11 +52,11 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error getting all registered plugins" + "Error getting all registered plugins", ); } }, - { detail: { tags: ["Management"], description: "List all Plugin Names" } } + { detail: { tags: ["Management"], description: "List all Plugin Names" } }, ) .post( "/update", @@ -67,14 +67,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) dbFunctions.updateConfig( fetching_interval, keep_data_for, - await hashApiKey(api_key) + await hashApiKey(api_key), ); return responseHandler.ok(set, "Updated DockStatAPI config"); } catch (error) { return responseHandler.error( set, "Error updating the DockStatAPI config", - error as string + error as string, ); } }, @@ -88,7 +88,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Update the current DockStatAPI config", }, - } + }, ) .get( "/package", @@ -110,7 +110,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error while reading package.json" + "Error while reading package.json", ); } }, @@ -119,5 +119,5 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Returns relevant information about the package.json", }, - } + }, ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index d3e88c75..e10f8c3a 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -1,5 +1,5 @@ import { Elysia, t } from "elysia"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; import { DockerHost } from "~/typings/docker"; diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index c86f6883..58bebcf9 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -1,6 +1,6 @@ import Docker from "dockerode"; import { Elysia } from "elysia"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { calculateCpuPercent, @@ -29,7 +29,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, pingError as string, - "Docker host connection failed" + "Docker host connection failed", ); } @@ -47,19 +47,19 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) set, reject, "An error occurred", - error + error, ); } if (!stats) { return responseHandler.reject( set, reject, - "No stats available" + "No stats available", ); } resolve(stats); }); - } + }, ); containers.push({ @@ -75,16 +75,16 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) } catch (containerError) { logger.error( "Error fetching container stats,", - containerError + containerError, ); } - }) + }), ); logger.debug(`Fetched stats for ${host.name}`); } catch (hostError) { logger.error("Error fetching containers for host,", hostError); } - }) + }), ); set.headers["Content-Type"] = "application/json"; @@ -94,7 +94,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, error as string, - "Failed to retrieve containers" + "Failed to retrieve containers", ); } }, @@ -104,7 +104,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) description: "Fetches all Containers and their statistics across all Hosts", }, - } + }, ) .get( @@ -117,7 +117,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) if (!host) { return responseHandler.simple_error( set, - `Host (${params.id}) not found` + `Host (${params.id}) not found`, ); } @@ -147,7 +147,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, error as string, - "Failed to retrieve host config" + "Failed to retrieve host config", ); } }, @@ -156,5 +156,5 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) tags: ["Statistics"], description: "Fetches the Host Stats for a specified Host", }, - } + }, ); diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index b3a6c52b..43292e2a 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -1,6 +1,6 @@ import { Elysia } from "elysia"; import type { ElysiaWS } from "elysia/dist/ws"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { calculateCpuPercent, diff --git a/src/routes/logs.ts b/src/routes/logs.ts index f5cf3cbe..d8289366 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -1,5 +1,5 @@ import { Elysia } from "elysia"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; export const backendLogs = new Elysia({ prefix: "/logs" }) diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 41304da8..2e07a2f9 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -8,8 +8,9 @@ import { getStackStatus, startStack, getAllStacksStatus, + removeStack, } from "~/core/stacks/controller"; -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; export const stackRoutes = new Elysia({ prefix: "/stacks" }) @@ -85,14 +86,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) "/start", async ({ set, body }) => { try { - if (!body.stack) { - throw new Error("Stack needed"); + if (!body.stackId) { + throw new Error("Stack ID needed"); } - await startStack(body.stack); - logger.info(`Started Stack (${body.stack})`); + await startStack(body.stackId); + logger.info(`Started Stack (${body.stackId})`); return responseHandler.ok( set, - `Stack ${body.stack} started successfully`, + `Stack ${body.stackId} started successfully`, ); } catch (error: any) { return responseHandler.error( @@ -105,7 +106,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) { detail: { tags: ["Stacks"], description: "Start a specific Stack" }, body: t.Object({ - stack: t.Any(), + stackId: t.Number(), }), }, ) @@ -113,14 +114,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) "/stop", async ({ set, body }) => { try { - if (!body.stack) { + if (!body.stackId) { throw new Error("Stack needed"); } - await stopStack(body.stack); - logger.info(`Stopped Stack (${body.stack})`); + await stopStack(body.stackId); + logger.info(`Stopped Stack (${body.stackId})`); return responseHandler.ok( set, - `Stack ${body.stack} stopped successfully`, + `Stack ${body.stackId} stopped successfully`, ); } catch (error: any) { return responseHandler.error( @@ -133,7 +134,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) { detail: { tags: ["Stacks"], description: "Stop the specified Stack" }, body: t.Object({ - stack: t.Any(), + stackId: t.Number(), }), }, ) @@ -141,14 +142,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) "/restart", async ({ set, body }) => { try { - if (!body.stack) { + if (!body.stackId) { throw new Error("Stack needed"); } - await restartStack(body.stack); - logger.info(`Restarted Stack (${body.stack})`); + await restartStack(body.stackId); + logger.info(`Restarted Stack (${body.stackId})`); return responseHandler.ok( set, - `Stack ${body.stack} restarted successfully`, + `Stack ${body.stackId} restarted successfully`, ); } catch (error: any) { return responseHandler.error( @@ -161,7 +162,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) { detail: { tags: ["Stacks"], description: "Restart a whole Stack" }, body: t.Object({ - stack: t.Any(), + stackId: t.Number(), }), }, ) @@ -169,14 +170,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) "/pull-images", async ({ set, body }) => { try { - if (!body.stack) { + if (!body.stackId) { throw new Error("Stack needed"); } - await pullStackImages(body.stack); - logger.info(`Pulled Stack images (${body.stack})`); + await pullStackImages(body.stackId); + logger.info(`Pulled Stack images (${body.stackId})`); return responseHandler.ok( set, - `Images for stack ${body.stack} pulled successfully`, + `Images for stack ${body.stackId} pulled successfully`, ); } catch (error: any) { return responseHandler.error( @@ -192,7 +193,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) description: "Runs `docker compose pull` on the provided Stack", }, body: t.Object({ - stack: t.Any(), + stackId: t.Number(), }), }, ) @@ -202,11 +203,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) try { let status; let res = {}; - if (query.stack_name) { - status = await getStackStatus(query.stack_name); + if (query.stackId) { + status = await getStackStatus(query.stackId); res = responseHandler.ok( set, - `Stack ${query.stack_name} status retrieved successfully`, + `Stack ${query.stackId} status retrieved successfully`, ); logger.info("Fetched Stack status"); } else { @@ -230,7 +231,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) "Fetches the current status of all containers for a specific Stack or if no Stack name is provided, for all Stacks", }, query: t.Object({ - stack_name: t.Any(), + stackId: t.Number(), }), }, ) @@ -255,4 +256,31 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) description: "Returns an Array of Stack-config-objects", }, }, + ) + + .delete( + "/", + async ({ set, body }) => { + try { + const { stackId } = body; + await removeStack(stackId); + logger.info(`Deleted Stack ${stackId}`); + return responseHandler.ok(set, `Stack ${stackId} deleted successfully`); + } catch (error: any) { + return responseHandler.error( + set, + error.message || error, + "Error deleting stack", + ); + } + }, + { + detail: { + tags: ["Stacks"], + description: "Delete a Stack", + }, + body: t.Object({ + stackId: t.Number(), + }), + }, ); diff --git a/src/tests/cleanup.ts b/src/tests/cleanup.ts index 163419ae..20b965b9 100644 --- a/src/tests/cleanup.ts +++ b/src/tests/cleanup.ts @@ -1,4 +1,4 @@ -import { dbFunctions } from "~/core/database/repository"; +import { dbFunctions } from "~/core/database"; import type { DockerHost } from "~/typings/docker"; import { findObjectByKey } from "~/core/utils/helpers"; From b01ef420e5277ecbfc6b7402bc59ed6cfa6fb17b Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 29 Mar 2025 23:59:18 +0100 Subject: [PATCH 227/369] Feat: Better Swagger documentation and minor fix in stack logic --- src/core/stacks/controller.ts | 4 +- src/core/utils/swagger-readme.ts | 64 ++++++++++++++++++++++++++++++++ src/index.ts | 5 ++- src/routes/api-config.ts | 17 +++++++-- src/routes/docker-manager.ts | 12 ++++-- src/routes/docker-stats.ts | 10 +++-- src/routes/logs.ts | 11 +++--- src/routes/stacks.ts | 31 ++++++++++++---- src/routes/utils.ts | 7 ++-- 9 files changed, 129 insertions(+), 32 deletions(-) create mode 100644 src/core/utils/swagger-readme.ts diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 7f1aeb9d..838532c5 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -189,13 +189,13 @@ export async function removeStack(stack_id: number): Promise { const stackName = await getStackName(stack_id); const stack = { - id: stack_id, + name: stackName, }; const stackPath = await getStackPath(stack as Stack); try { - await rm("stackPath", { recursive: true }); + await rm(stackPath, { recursive: true }); } catch (error: any) { if (error.code === "ENOENT") { console.log("Directory doesn't exist"); diff --git a/src/core/utils/swagger-readme.ts b/src/core/utils/swagger-readme.ts new file mode 100644 index 00000000..ff30a8bf --- /dev/null +++ b/src/core/utils/swagger-readme.ts @@ -0,0 +1,64 @@ +export const swaggerReadme: string = ` +![Docker](https://img.shields.io/badge/Docker-2CA5E0?style=flat&logo=docker&logoColor=white) +![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white) + +Docker infrastructure management API with real-time monitoring and orchestration capabilities. + +## Key Features + +- **Stack Orchestration** + Deploy/update Docker stacks (compose v3+) with custom configurations +- **Container Monitoring** + Real-time metrics (CPU/RAM/status) across multiple Docker hosts +- **Centralized Logging** + Structured log management with retention policies and filtering +- **Host Management** + Multi-host configuration with connection health checks +- **Plugin System** + Extensible architecture for custom monitoring integrations + +## Installation & Setup + +**Prerequisites**: +- Node.js 18+ +- Docker Engine 23+ +- Bun runtime + +\`\`\`bash +# Clone repo +git clone https://github.com/Its4Nik/DockStatAPI.git +cd DockStatAPI +# Install dependencies +bun install + +# Start development server +bun run dev +\`\`\` + +## Configuration + +**Environment Variables**: +\`\`\`ini +PAD_NEW_LINES=true +NODE_ENV=production +LOG_LEVEL=info +\`\`\` + +## Security + +1. Always use HTTPS in production +2. Rotate API keys regularly +3. Restrict host connections to trusted networks +4. Enable Docker Engine TLS authentication + +## Contributing + +1. Fork repository +2. Create feature branch (\`feat/my-feature\`) +3. Submit PR with detailed description + +**Code Style**: +- TypeScript strict mode +- Elysia framework conventions +- Prettier formatting +`; diff --git a/src/index.ts b/src/index.ts index c7d7f11d..5cc5af38 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import { validateApiKey } from "./middleware/auth"; import { monitorDockerEvents } from "./core/docker/monitor"; import { liveLogs } from "./routes/live-logs"; import { utilRoutes } from "./routes/utils"; +import { swaggerReadme } from "./core/utils/swagger-readme"; console.log(""); @@ -31,8 +32,8 @@ export const DockStatAPI = new Elysia() documentation: { info: { title: "DockStatAPI", - version: "2.1.0", - description: "Docker monitoring API with plugin support", + version: "3.0.0", + description: swaggerReadme, }, components: { securitySchemes: { diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 2059b0e2..9861d3f9 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -39,7 +39,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) { detail: { tags: ["Management"], - description: "Returns DockStatAPI's config", + description: + "Returns current API configuration including data retention policies and security settings", }, }, ) @@ -56,7 +57,13 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) ); } }, - { detail: { tags: ["Management"], description: "List all Plugin Names" } }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + }, + }, ) .post( "/update", @@ -86,7 +93,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) }), detail: { tags: ["Management"], - description: "Update the current DockStatAPI config", + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", }, }, ) @@ -117,7 +125,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) { detail: { tags: ["Management"], - description: "Returns relevant information about the package.json", + description: + "Displays package metadata including dependencies, contributors, and licensing information", }, }, ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index e10f8c3a..635b78bb 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -23,7 +23,8 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) { detail: { tags: ["Management"], - description: "Add a new Host as Monitoring target", + description: + "Registers a new Docker host to the monitoring system with connection details", }, body: t.Object({ name: t.String(), @@ -51,7 +52,8 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) { detail: { tags: ["Management"], - description: "Update an already existing target's config", + description: + "Modifies existing Docker host configuration parameters (name, address, security)", }, body: t.Object({ id: t.Number(), @@ -81,7 +83,8 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) { detail: { tags: ["Management"], - description: "Returns an Array of Host-config-objects", + description: + "Lists all configured Docker hosts with their connection settings", }, }, ) @@ -104,7 +107,8 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) { detail: { tags: ["Management"], - description: "Delete an existing host", + description: + "Removes Docker host from monitoring system and clears associated data", }, params: t.Object({ id: t.Number(), diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 58bebcf9..a600af3a 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -64,7 +64,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) containers.push({ id: containerInfo.Id, - hostId: host.id as string, + hostId: `${host.id}`, name: containerInfo.Names[0].replace(/^\//, ""), image: containerInfo.Image, status: containerInfo.Status, @@ -102,7 +102,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) detail: { tags: ["Statistics"], description: - "Fetches all Containers and their statistics across all Hosts", + "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", }, }, ) @@ -125,7 +125,8 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) const info: DockerInfo = await docker.info(); const config: HostStats = { - hostId: host.name, + hostId: host.id as number, + hostName: host.name, dockerVersion: info.ServerVersion, apiVersion: info.Driver, os: info.OperatingSystem, @@ -154,7 +155,8 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) { detail: { tags: ["Statistics"], - description: "Fetches the Host Stats for a specified Host", + description: + "Provides detailed system metrics and Docker runtime information for specified host", }, }, ); diff --git a/src/routes/logs.ts b/src/routes/logs.ts index d8289366..dc7fc374 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -20,7 +20,8 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], - description: "Retrieves all Logs which have been saved in the Database", + description: + "Retrieves complete application log history from persistent storage", }, }, ) @@ -42,7 +43,8 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], - description: "Retrieves all Logs with the specified level", + description: + "Filters logs by severity level (debug, info, warn, error, fatal)", }, }, ) @@ -64,7 +66,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], - description: "Deletes all Logs which are saved in the Database", + description: "Purges all historical log records from the database", }, }, ) @@ -86,8 +88,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) { detail: { tags: ["Management"], - description: - "Deletes all Logs with the specified Level inside the Database", + description: "Clears log entries matching specified severity level", }, }, ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 2e07a2f9..eea41609 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -68,7 +68,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) detail: { tags: ["Stacks"], description: - "Deploy a Stack, either with a prebuilt one or provide your own structure", + "Deploys a new Docker stack using a provided compose specification, allowing custom configurations and image updates", }, body: t.Object({ compose_spec: t.Any(), @@ -104,7 +104,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) } }, { - detail: { tags: ["Stacks"], description: "Start a specific Stack" }, + detail: { + tags: ["Stacks"], + description: + "Initiates a Docker stack, starting all associated containers", + }, body: t.Object({ stackId: t.Number(), }), @@ -132,7 +136,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) } }, { - detail: { tags: ["Stacks"], description: "Stop the specified Stack" }, + detail: { + tags: ["Stacks"], + description: + "Halts a running Docker stack and its containers while preserving configurations", + }, body: t.Object({ stackId: t.Number(), }), @@ -160,7 +168,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) } }, { - detail: { tags: ["Stacks"], description: "Restart a whole Stack" }, + detail: { + tags: ["Stacks"], + description: + "Performs full stack restart - stops and restarts all stack components in sequence", + }, body: t.Object({ stackId: t.Number(), }), @@ -190,7 +202,8 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) { detail: { tags: ["Stacks"], - description: "Runs `docker compose pull` on the provided Stack", + description: + "Updates container images for a stack using Docker's pull mechanism (requires stack ID)", }, body: t.Object({ stackId: t.Number(), @@ -228,7 +241,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) detail: { tags: ["Stacks"], description: - "Fetches the current status of all containers for a specific Stack or if no Stack name is provided, for all Stacks", + "Retrieves operational status for either a specific stack (by ID) or all managed stacks", }, query: t.Object({ stackId: t.Number(), @@ -253,7 +266,8 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) { detail: { tags: ["Stacks"], - description: "Returns an Array of Stack-config-objects", + description: + "Lists all registered stacks with their complete configuration details", }, }, ) @@ -277,7 +291,8 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) { detail: { tags: ["Stacks"], - description: "Delete a Stack", + description: + "Permanently removes a stack configuration and cleans up associated resources", }, body: t.Object({ stackId: t.Number(), diff --git a/src/routes/utils.ts b/src/routes/utils.ts index cb8b9424..b61e0e7d 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -32,14 +32,15 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( return responseHandler.error( set, error.message || error, - "Error getting DockStatAPI information" + "Error getting DockStatAPI information", ); } }, { detail: { tags: ["Utils"], - description: "Shows general information about DockStatAPI", + description: + "Retrieves DockStatAPI metadata including version, author information, dependencies, and licensing details", }, - } + }, ); From 8ac97e71cc1992d1e1fde9da477b089f7ab070c7 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 29 Mar 2025 23:01:01 +0000 Subject: [PATCH 228/369] Update dependency graphs --- dependency-graph.dot | 84 ++- dependency-graph.mmd | 336 +++++---- dependency-graph.svg | 1646 ++++++++++++++++++++++++------------------ 3 files changed, 1183 insertions(+), 883 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index 254698fb..a3cdc90c 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -10,38 +10,65 @@ strict digraph "dependency-cruiser output"{ subgraph "cluster_fs" {label="fs" "fs/promises" [label= tooltip="promises" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] } "package.json" [label= tooltip="package.json" URL="package.json" fillcolor="#ffee44"] "path" [label= tooltip="path" URL="https://nodejs.org/api/path.html" color="grey" fontcolor="grey"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/config.ts" [label= tooltip="config.ts" URL="src/core/database/config.ts" fillcolor="#ddfeff"] } } } + "src/core/database/config.ts" -> "src/core/database/database.ts" + "src/core/database/config.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/containerStats.ts" [label= tooltip="containerStats.ts" URL="src/core/database/containerStats.ts" fillcolor="#ddfeff"] } } } + "src/core/database/containerStats.ts" -> "src/core/database/database.ts" + "src/core/database/containerStats.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/database.ts" [label= tooltip="database.ts" URL="src/core/database/database.ts" fillcolor="#ddfeff"] } } } + "src/core/database/database.ts" -> "bun:sqlite" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/dockerHosts.ts" [label= tooltip="dockerHosts.ts" URL="src/core/database/dockerHosts.ts" fillcolor="#ddfeff"] } } } + "src/core/database/dockerHosts.ts" -> "src/core/database/database.ts" + "src/core/database/dockerHosts.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + "src/core/database/dockerHosts.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/helper.ts" [label= tooltip="helper.ts" URL="src/core/database/helper.ts" fillcolor="#ddfeff"] } } } "src/core/database/helper.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/repository.ts" [label= tooltip="repository.ts" URL="src/core/database/repository.ts" fillcolor="#ddfeff"] } } } - "src/core/database/repository.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - "src/core/database/repository.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] - "src/core/database/repository.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/database/repository.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/database/repository.ts" -> "bun:sqlite" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/hostStats.ts" [label= tooltip="hostStats.ts" URL="src/core/database/hostStats.ts" fillcolor="#ddfeff"] } } } + "src/core/database/hostStats.ts" -> "src/core/database/database.ts" + "src/core/database/hostStats.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + "src/core/database/hostStats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/index.ts" [label= tooltip="index.ts" URL="src/core/database/index.ts" fillcolor="#ddfeff"] } } } + "src/core/database/index.ts" -> "src/core/database/config.ts" [arrowhead="normalnoneodot"] + "src/core/database/index.ts" -> "src/core/database/containerStats.ts" [arrowhead="normalnoneodot"] + "src/core/database/index.ts" -> "src/core/database/database.ts" + "src/core/database/index.ts" -> "src/core/database/dockerHosts.ts" [arrowhead="normalnoneodot"] + "src/core/database/index.ts" -> "src/core/database/hostStats.ts" [arrowhead="normalnoneodot"] + "src/core/database/index.ts" -> "src/core/database/logs.ts" [arrowhead="normalnoneodot"] + "src/core/database/index.ts" -> "src/core/database/stacks.ts" [arrowhead="normalnoneodot"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/logs.ts" [label= tooltip="logs.ts" URL="src/core/database/logs.ts" fillcolor="#ddfeff"] } } } + "src/core/database/logs.ts" -> "src/core/database/database.ts" + "src/core/database/logs.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + "src/core/database/logs.ts" -> "src/typings/websocket.ts" + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/stacks.ts" [label= tooltip="stacks.ts" URL="src/core/database/stacks.ts" fillcolor="#ddfeff"] } } } + "src/core/database/stacks.ts" -> "src/core/database/database.ts" + "src/core/database/stacks.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] + "src/core/database/stacks.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/database/stacks.ts" -> "src/typings/docker-compose.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/client.ts" [label= tooltip="client.ts" URL="src/core/docker/client.ts" fillcolor="#ddfeff"] } } } "src/core/docker/client.ts" -> "src/core/utils/logger.ts" "src/core/docker/client.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/monitor.ts" [label= tooltip="monitor.ts" URL="src/core/docker/monitor.ts" fillcolor="#ddfeff"] } } } "src/core/docker/monitor.ts" -> "src/core/plugins/plugin-manager.ts" - "src/core/docker/monitor.ts" -> "src/core/database/repository.ts" + "src/core/docker/monitor.ts" -> "src/core/database/index.ts" "src/core/docker/monitor.ts" -> "src/core/docker/client.ts" "src/core/docker/monitor.ts" -> "src/core/utils/logger.ts" "src/core/docker/monitor.ts" -> "src/typings/docker.ts" "src/core/docker/monitor.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] "src/core/docker/monitor.ts" -> "bun" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/scheduler.ts" [label= tooltip="scheduler.ts" URL="src/core/docker/scheduler.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/scheduler.ts" -> "src/core/database/repository.ts" + "src/core/docker/scheduler.ts" -> "src/core/database/index.ts" "src/core/docker/scheduler.ts" -> "src/core/docker/store-host-stats.ts" "src/core/docker/scheduler.ts" -> "src/core/docker/store-container-stats.ts" "src/core/docker/scheduler.ts" -> "src/core/utils/logger.ts" "src/core/docker/scheduler.ts" -> "src/typings/database.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-container-stats.ts" [label= tooltip="store-container-stats.ts" URL="src/core/docker/store-container-stats.ts" fillcolor="#ddfeff"] } } } "src/core/docker/store-container-stats.ts" -> "src/core/utils/logger.ts" - "src/core/docker/store-container-stats.ts" -> "src/core/database/repository.ts" + "src/core/docker/store-container-stats.ts" -> "src/core/database/index.ts" "src/core/docker/store-container-stats.ts" -> "src/core/docker/client.ts" "src/core/docker/store-container-stats.ts" -> "src/core/utils/calculations.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-host-stats.ts" [label= tooltip="store-host-stats.ts" URL="src/core/docker/store-host-stats.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/store-host-stats.ts" -> "src/core/database/repository.ts" + "src/core/docker/store-host-stats.ts" -> "src/core/database/index.ts" "src/core/docker/store-host-stats.ts" -> "src/core/docker/client.ts" "src/core/docker/store-host-stats.ts" -> "src/core/utils/logger.ts" "src/core/docker/store-host-stats.ts" -> "src/typings/docker.ts" @@ -58,26 +85,28 @@ strict digraph "dependency-cruiser output"{ "src/core/plugins/plugin-manager.ts" -> "src/typings/plugin.ts" [arrowhead="onormal" penwidth="1.0"] "src/core/plugins/plugin-manager.ts" -> "events" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/stacks" {label="stacks" "src/core/stacks/controller.ts" [label= tooltip="controller.ts" URL="src/core/stacks/controller.ts" fillcolor="#ddfeff"] } } } - "src/core/stacks/controller.ts" -> "src/core/database/repository.ts" + "src/core/stacks/controller.ts" -> "src/core/database/index.ts" "src/core/stacks/controller.ts" -> "src/core/utils/logger.ts" "src/core/stacks/controller.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] "src/core/stacks/controller.ts" -> "src/typings/docker-compose.ts" [arrowhead="onormal" penwidth="1.0"] + "src/core/stacks/controller.ts" -> "bun" + "src/core/stacks/controller.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/index.ts" [label= tooltip="index.ts" URL="src/core/trpc/index.ts" fillcolor="#ddfeff"] } } } "src/core/trpc/index.ts" -> "src/core/trpc/router.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/api-config.procedure.ts" [label= tooltip="api-config.procedure.ts" URL="src/core/trpc/procedures/api-config.procedure.ts" fillcolor="#ddfeff"] } } } } "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/database/index.ts" "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/logger.ts" "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/package-json.ts" "src/core/trpc/procedures/api-config.procedure.ts" -> "src/typings/database.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-manager.procedure.ts" [label= tooltip="docker-manager.procedure.ts" URL="src/core/trpc/procedures/docker-manager.procedure.ts" fillcolor="#ddfeff"] } } } } "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/database/index.ts" "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/utils/logger.ts" "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/typings/docker.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-stats.procedure.ts" [label= tooltip="docker-stats.procedure.ts" URL="src/core/trpc/procedures/docker-stats.procedure.ts" fillcolor="#ddfeff"] } } } } "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/database/index.ts" "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/docker/client.ts" "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/calculations.ts" "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/logger.ts" @@ -85,11 +114,11 @@ strict digraph "dependency-cruiser output"{ "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/logs.procedure.ts" [label= tooltip="logs.procedure.ts" URL="src/core/trpc/procedures/logs.procedure.ts" fillcolor="#ddfeff"] } } } } "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/database/index.ts" "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/utils/logger.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/stacks.procedure.ts" [label= tooltip="stacks.procedure.ts" URL="src/core/trpc/procedures/stacks.procedure.ts" fillcolor="#ddfeff"] } } } } "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/database/repository.ts" + "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/database/index.ts" "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/stacks/controller.ts" "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/utils/logger.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/router.ts" [label= tooltip="router.ts" URL="src/core/trpc/router.ts" fillcolor="#ddfeff"] } } } @@ -105,23 +134,25 @@ strict digraph "dependency-cruiser output"{ "src/core/utils/change-me-checker.ts" -> "src/core/utils/logger.ts" "src/core/utils/change-me-checker.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/logger.ts" [label= tooltip="logger.ts" URL="src/core/utils/logger.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/logger.ts" -> "src/core/database/repository.ts" [arrowhead="normalnoneodot"] + "src/core/utils/logger.ts" -> "src/core/database/index.ts" [arrowhead="normalnoneodot"] "src/core/utils/logger.ts" -> "src/routes/live-logs.ts" [arrowhead="normalnoneodot"] - "src/core/utils/logger.ts" -> "src/typings/websocket.ts" + "src/core/utils/logger.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] "src/core/utils/logger.ts" -> "path" [style="dashed" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/package-json.ts" [label= tooltip="package-json.ts" URL="src/core/utils/package-json.ts" fillcolor="#ddfeff"] } } } "src/core/utils/package-json.ts" -> "package.json" subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/response-handler.ts" [label= tooltip="response-handler.ts" URL="src/core/utils/response-handler.ts" fillcolor="#ddfeff"] } } } "src/core/utils/response-handler.ts" -> "src/core/utils/logger.ts" "src/core/utils/response-handler.ts" -> "src/typings/elysiajs.ts" [arrowhead="onormal" penwidth="1.0"] + subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/swagger-readme.ts" [label= tooltip="swagger-readme.ts" URL="src/core/utils/swagger-readme.ts" fillcolor="#ddfeff"] } } } subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } "src/index.ts" -> "src/core/docker/monitor.ts" + "src/index.ts" -> "src/core/utils/swagger-readme.ts" "src/index.ts" -> "src/middleware/auth.ts" "src/index.ts" -> "src/routes/live-logs.ts" "src/index.ts" -> "src/routes/stacks.ts" "src/index.ts" -> "src/routes/utils.ts" "src/index.ts" -> "src/typings/database.ts" - "src/index.ts" -> "src/core/database/repository.ts" + "src/index.ts" -> "src/core/database/index.ts" "src/index.ts" -> "src/core/docker/scheduler.ts" "src/index.ts" -> "src/core/plugins/loader.ts" "src/index.ts" -> "src/core/trpc/index.ts" @@ -132,12 +163,12 @@ strict digraph "dependency-cruiser output"{ "src/index.ts" -> "src/routes/docker-websocket.ts" "src/index.ts" -> "src/routes/logs.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/middleware" {label="middleware" "src/middleware/auth.ts" [label= tooltip="auth.ts" URL="src/middleware/auth.ts" fillcolor="#ddfeff"] } } - "src/middleware/auth.ts" -> "src/core/database/repository.ts" + "src/middleware/auth.ts" -> "src/core/database/index.ts" "src/middleware/auth.ts" -> "src/core/utils/logger.ts" "src/middleware/auth.ts" -> "src/typings/database.ts" "src/middleware/auth.ts" -> "src/typings/elysiajs.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/api-config.ts" [label= tooltip="api-config.ts" URL="src/routes/api-config.ts" fillcolor="#ddfeff"] } } - "src/routes/api-config.ts" -> "src/core/database/repository.ts" + "src/routes/api-config.ts" -> "src/core/database/index.ts" "src/routes/api-config.ts" -> "src/core/plugins/plugin-manager.ts" "src/routes/api-config.ts" -> "src/core/utils/logger.ts" "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" @@ -145,11 +176,12 @@ strict digraph "dependency-cruiser output"{ "src/routes/api-config.ts" -> "src/middleware/auth.ts" "src/routes/api-config.ts" -> "src/typings/database.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-manager.ts" [label= tooltip="docker-manager.ts" URL="src/routes/docker-manager.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-manager.ts" -> "src/core/database/repository.ts" + "src/routes/docker-manager.ts" -> "src/core/database/index.ts" "src/routes/docker-manager.ts" -> "src/core/utils/logger.ts" "src/routes/docker-manager.ts" -> "src/core/utils/response-handler.ts" + "src/routes/docker-manager.ts" -> "src/typings/docker.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-stats.ts" [label= tooltip="docker-stats.ts" URL="src/routes/docker-stats.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-stats.ts" -> "src/core/database/repository.ts" + "src/routes/docker-stats.ts" -> "src/core/database/index.ts" "src/routes/docker-stats.ts" -> "src/core/docker/client.ts" "src/routes/docker-stats.ts" -> "src/core/utils/calculations.ts" "src/routes/docker-stats.ts" -> "src/core/utils/logger.ts" @@ -157,7 +189,7 @@ strict digraph "dependency-cruiser output"{ "src/routes/docker-stats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] "src/routes/docker-stats.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-websocket.ts" [label= tooltip="docker-websocket.ts" URL="src/routes/docker-websocket.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-websocket.ts" -> "src/core/database/repository.ts" + "src/routes/docker-websocket.ts" -> "src/core/database/index.ts" "src/routes/docker-websocket.ts" -> "src/core/docker/client.ts" "src/routes/docker-websocket.ts" -> "src/core/utils/calculations.ts" "src/routes/docker-websocket.ts" -> "src/core/utils/logger.ts" @@ -167,10 +199,10 @@ strict digraph "dependency-cruiser output"{ "src/routes/live-logs.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] "src/routes/live-logs.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/logs.ts" [label= tooltip="logs.ts" URL="src/routes/logs.ts" fillcolor="#ddfeff"] } } - "src/routes/logs.ts" -> "src/core/database/repository.ts" + "src/routes/logs.ts" -> "src/core/database/index.ts" "src/routes/logs.ts" -> "src/core/utils/logger.ts" subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/stacks.ts" [label= tooltip="stacks.ts" URL="src/routes/stacks.ts" fillcolor="#ddfeff"] } } - "src/routes/stacks.ts" -> "src/core/database/repository.ts" + "src/routes/stacks.ts" -> "src/core/database/index.ts" "src/routes/stacks.ts" -> "src/core/stacks/controller.ts" "src/routes/stacks.ts" -> "src/core/utils/logger.ts" "src/routes/stacks.ts" -> "src/core/utils/response-handler.ts" diff --git a/dependency-graph.mmd b/dependency-graph.mmd index ad13d50c..bbe9dd96 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -11,207 +11,239 @@ subgraph 0["src"] subgraph 2["core"] subgraph 3["docker"] 4["monitor.ts"] -N["client.ts"] -Z["scheduler.ts"] -10["store-host-stats.ts"] -12["store-container-stats.ts"] +V["client.ts"] +19["scheduler.ts"] +1A["store-host-stats.ts"] +1C["store-container-stats.ts"] end subgraph 6["plugins"] 7["plugin-manager.ts"] -14["loader.ts"] +1E["loader.ts"] end subgraph 9["utils"] A["logger.ts"] -V["response-handler.ts"] -X["package-json.ts"] -13["calculations.ts"] -16["change-me-checker.ts"] +W["swagger-readme.ts"] +15["response-handler.ts"] +17["package-json.ts"] +1D["calculations.ts"] +1F["change-me-checker.ts"] end subgraph C["database"] -D["repository.ts"] -F["helper.ts"] +D["index.ts"] +E["config.ts"] +F["database.ts"] +H["helper.ts"] +I["containerStats.ts"] +J["dockerHosts.ts"] +M["hostStats.ts"] +N["logs.ts"] +P["stacks.ts"] end -subgraph S["stacks"] -T["controller.ts"] +subgraph 11["stacks"] +12["controller.ts"] end -subgraph 18["trpc"] -19["index.ts"] -1A["router.ts"] -subgraph 1B["procedures"] -1C["api-config.procedure.ts"] -1E["docker-manager.procedure.ts"] -1F["docker-stats.procedure.ts"] -1G["logs.procedure.ts"] -1H["stacks.procedure.ts"] +subgraph 1G["trpc"] +1H["index.ts"] +1I["router.ts"] +subgraph 1J["procedures"] +1K["api-config.procedure.ts"] +1M["docker-manager.procedure.ts"] +1N["docker-stats.procedure.ts"] +1O["logs.procedure.ts"] +1P["stacks.procedure.ts"] end -1D["trpc.ts"] +1L["trpc.ts"] end end -subgraph G["typings"] -H["database.ts"] -I["docker.ts"] -L["websocket.ts"] -M["plugin.ts"] -Q["elysiajs.ts"] -U["docker-compose.ts"] -11["dockerode.ts"] +subgraph K["typings"] +L["docker.ts"] +O["websocket.ts"] +Q["database.ts"] +R["docker-compose.ts"] +U["plugin.ts"] +Z["elysiajs.ts"] +1B["dockerode.ts"] end -subgraph J["routes"] -K["live-logs.ts"] -R["stacks.ts"] -W["utils.ts"] -1I["api-config.ts"] -1J["docker-manager.ts"] -1K["docker-stats.ts"] -1L["docker-websocket.ts"] -1N["logs.ts"] +subgraph S["routes"] +T["live-logs.ts"] +10["stacks.ts"] +16["utils.ts"] +1Q["api-config.ts"] +1R["docker-manager.ts"] +1S["docker-stats.ts"] +1T["docker-websocket.ts"] +1V["logs.ts"] end -subgraph O["middleware"] -P["auth.ts"] +subgraph X["middleware"] +Y["auth.ts"] end end 5["bun"] 8["events"] B["path"] -E["bun:sqlite"] -Y["package.json"] -subgraph 15["fs"] -17["promises"] +G["bun:sqlite"] +subgraph 13["fs"] +14["promises"] end -1M["stream"] +18["package.json"] +1U["stream"] 1-->4 -1-->P -1-->K -1-->R 1-->W -1-->H +1-->Y +1-->T +1-->10 +1-->16 +1-->Q 1-->D -1-->Z -1-->14 1-->19 +1-->1E +1-->1H 1-->A -1-->1I -1-->1J -1-->1K -1-->1L -1-->1N +1-->1Q +1-->1R +1-->1S +1-->1T +1-->1V 4-->7 4-->D -4-->N +4-->V 4-->A -4-->I -4-->I +4-->L +4-->L 4-->5 7-->A -7-->I -7-->M +7-->L +7-->U 7-->8 A-->D -A-->K -A-->L +A-->T +A-->O A-->B -D-->F -D-->A -D-->H -D-->I D-->E -F-->A -K-->A -K-->L -M-->I -N-->A -N-->I -P-->D -P-->A +D-->I +D-->F +D-->J +D-->M +D-->N +D-->P +E-->F +E-->H +F-->G +H-->A +I-->F +I-->H +J-->F +J-->H +J-->L +M-->F +M-->H +M-->L +N-->F +N-->H +N-->O +P-->F P-->H P-->Q -R-->D -R-->T -R-->A -R-->V -T-->D +P-->R T-->A -T-->H -T-->U +T-->O +U-->L V-->A -V-->Q -W-->X -W-->V -X-->Y -Z-->D -Z-->10 -Z-->12 -Z-->A -Z-->H +V-->L +Y-->D +Y-->A +Y-->Q +Y-->Z 10-->D -10-->N +10-->12 10-->A -10-->I -10-->11 -12-->A +10-->15 12-->D -12-->N -12-->13 -14-->16 -14-->A -14-->7 -14-->15 -14-->B -16-->A +12-->A +12-->Q +12-->R +12-->5 +12-->14 +15-->A +15-->Z 16-->17 +16-->15 +17-->18 +19-->D 19-->1A -1A-->1C -1A-->1E -1A-->1F -1A-->1G -1A-->1H -1A-->1D -1C-->1D -1C-->D +19-->1C +19-->A +19-->Q +1A-->D +1A-->V +1A-->A +1A-->L +1A-->1B 1C-->A -1C-->X -1C-->H -1E-->1D -1E-->D +1C-->D +1C-->V +1C-->1D +1E-->1F 1E-->A -1E-->I -1F-->1D -1F-->D -1F-->N -1F-->13 +1E-->7 +1E-->13 +1E-->B 1F-->A -1F-->I -1F-->11 -1G-->1D -1G-->D -1G-->A -1H-->1D -1H-->D -1H-->T -1H-->A -1I-->D -1I-->7 -1I-->A -1I-->X -1I-->V -1I-->P -1I-->H -1J-->D -1J-->A -1J-->V +1F-->14 +1H-->1I +1I-->1K +1I-->1M +1I-->1N +1I-->1O +1I-->1P +1I-->1L +1K-->1L 1K-->D -1K-->N -1K-->13 1K-->A -1K-->V -1K-->I -1K-->11 -1L-->D -1L-->N -1L-->13 -1L-->A -1L-->V -1L-->1M +1K-->17 +1K-->Q +1M-->1L +1M-->D +1M-->A +1M-->L +1N-->1L 1N-->D +1N-->V +1N-->1D 1N-->A +1N-->L +1N-->1B +1O-->1L +1O-->D +1O-->A +1P-->1L +1P-->D +1P-->12 +1P-->A +1Q-->D +1Q-->7 +1Q-->A +1Q-->17 +1Q-->15 +1Q-->Y +1Q-->Q +1R-->D +1R-->A +1R-->15 +1R-->L +1S-->D +1S-->V +1S-->1D +1S-->A +1S-->15 +1S-->L +1S-->1B +1T-->D +1T-->V +1T-->1D +1T-->A +1T-->15 +1T-->1U +1V-->D +1V-->A diff --git a/dependency-graph.svg b/dependency-graph.svg index 715f66a2..c1e4807d 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,82 +4,82 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/trpc - -trpc + +trpc cluster_src/core/trpc/procedures - -procedures + +procedures cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -87,8 +87,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -96,8 +96,8 @@ events - -events + +events @@ -105,8 +105,8 @@ fs - -fs + +fs @@ -114,8 +114,8 @@ fs/promises - -promises + +promises @@ -123,8 +123,8 @@ package.json - -package.json + +package.json @@ -132,1195 +132,1431 @@ path - -path + +path - + +src/core/database/config.ts + + +config.ts + + + + + +src/core/database/database.ts + + +database.ts + + + + + +src/core/database/config.ts->src/core/database/database.ts + + + + + src/core/database/helper.ts - - -helper.ts + + +helper.ts + + +src/core/database/config.ts->src/core/database/helper.ts + + + + + + + +src/core/database/database.ts->bun:sqlite + + + - + src/core/utils/logger.ts - - -logger.ts + + +logger.ts - + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + + + + +src/core/database/containerStats.ts + + +containerStats.ts + + + + + +src/core/database/containerStats.ts->src/core/database/database.ts + + + + + +src/core/database/containerStats.ts->src/core/database/helper.ts + + + + + + + +src/core/database/dockerHosts.ts + + +dockerHosts.ts + + + + + +src/core/database/dockerHosts.ts->src/core/database/database.ts + + + + + +src/core/database/dockerHosts.ts->src/core/database/helper.ts + + + + + + + +src/typings/docker.ts + + +docker.ts + + + + + +src/core/database/dockerHosts.ts->src/typings/docker.ts + + - + src/core/utils/logger.ts->path - - + + - - -src/core/database/repository.ts - - -repository.ts + + +src/core/database/index.ts + + +index.ts - - -src/core/utils/logger.ts->src/core/database/repository.ts - - - - + + +src/core/utils/logger.ts->src/core/database/index.ts + + + + + + + +src/typings/websocket.ts + + +websocket.ts + + + + + +src/core/utils/logger.ts->src/typings/websocket.ts + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + - - -src/typings/websocket.ts - - -websocket.ts + + +src/core/database/hostStats.ts + + +hostStats.ts - - -src/core/utils/logger.ts->src/typings/websocket.ts - - + + +src/core/database/hostStats.ts->src/core/database/database.ts + + - - -src/core/database/repository.ts->bun:sqlite - - + + +src/core/database/hostStats.ts->src/core/database/helper.ts + + + + - - -src/core/database/repository.ts->src/core/database/helper.ts - - - - + + +src/core/database/hostStats.ts->src/typings/docker.ts + + - - -src/core/database/repository.ts->src/core/utils/logger.ts - - - - + + +src/core/database/index.ts->src/core/database/config.ts + + + + + + + +src/core/database/index.ts->src/core/database/database.ts + + + + + +src/core/database/index.ts->src/core/database/containerStats.ts + + + + + + + +src/core/database/index.ts->src/core/database/dockerHosts.ts + + + + + + + +src/core/database/index.ts->src/core/database/hostStats.ts + + + + + + + +src/core/database/logs.ts + + +logs.ts + + + + + +src/core/database/index.ts->src/core/database/logs.ts + + + + + + + +src/core/database/stacks.ts + + +stacks.ts + + + + + +src/core/database/index.ts->src/core/database/stacks.ts + + + + + + + +src/core/database/logs.ts->src/core/database/database.ts + + + + + +src/core/database/logs.ts->src/core/database/helper.ts + + + + + + + +src/core/database/logs.ts->src/typings/websocket.ts + + + + + +src/core/database/stacks.ts->src/core/database/database.ts + + + + + +src/core/database/stacks.ts->src/core/database/helper.ts + + + + - + src/typings/database.ts - - -database.ts + + +database.ts - - -src/core/database/repository.ts->src/typings/database.ts - - + + +src/core/database/stacks.ts->src/typings/database.ts + + - - -src/typings/docker.ts - - -docker.ts + + +src/typings/docker-compose.ts + + +docker-compose.ts - - -src/core/database/repository.ts->src/typings/docker.ts - - + + +src/core/database/stacks.ts->src/typings/docker-compose.ts + + - + src/core/docker/client.ts - - -client.ts + + +client.ts - - -src/core/docker/client.ts->src/core/utils/logger.ts - - - - + src/core/docker/client.ts->src/typings/docker.ts - - + + + + + +src/core/docker/client.ts->src/core/utils/logger.ts + + - + src/core/docker/monitor.ts - - -monitor.ts + + +monitor.ts - + src/core/docker/monitor.ts->bun - - + + + + + +src/core/docker/monitor.ts->src/typings/docker.ts + + - + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + - - -src/core/docker/monitor.ts->src/core/database/repository.ts - - - - - -src/core/docker/monitor.ts->src/typings/docker.ts - - + + +src/core/docker/monitor.ts->src/core/database/index.ts + + - + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + - + src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +plugin-manager.ts - + src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - - - - -src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + + + + +src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts + + - + src/typings/plugin.ts - - -plugin.ts + + +plugin.ts - + src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - - -src/core/docker/scheduler.ts->src/core/database/repository.ts - - + + +src/core/docker/scheduler.ts->src/core/database/index.ts + + - + src/core/docker/scheduler.ts->src/typings/database.ts - - + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + + + + +src/core/docker/store-host-stats.ts->src/typings/docker.ts + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - - -src/core/docker/store-host-stats.ts->src/core/database/repository.ts - - - - - -src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + +src/core/docker/store-host-stats.ts->src/core/database/index.ts + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + src/typings/dockerode.ts - - -dockerode.ts + + +dockerode.ts - + src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + - - -src/core/docker/store-container-stats.ts->src/core/database/repository.ts - - + + +src/core/docker/store-container-stats.ts->src/core/database/index.ts + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - - -calculations.ts + + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts + + +src/core/stacks/controller.ts->bun + + + + + +src/core/stacks/controller.ts->fs/promises + + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - - -src/core/stacks/controller.ts->src/core/database/repository.ts - - + + +src/core/stacks/controller.ts->src/core/database/index.ts + + - + src/core/stacks/controller.ts->src/typings/database.ts - - - - - -src/typings/docker-compose.ts - - -docker-compose.ts - - + + - + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + - + src/core/trpc/index.ts - - -index.ts + + +index.ts - + src/core/trpc/router.ts - - -router.ts + + +router.ts - + src/core/trpc/index.ts->src/core/trpc/router.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts - - -api-config.procedure.ts + + +api-config.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - + + - + src/core/trpc/trpc.ts - - -trpc.ts + + +trpc.ts - + src/core/trpc/router.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/docker-manager.procedure.ts - - -docker-manager.procedure.ts + + +docker-manager.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts - - -docker-stats.procedure.ts + + +docker-stats.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts - - -logs.procedure.ts + + +logs.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts - - -stacks.procedure.ts + + +stacks.procedure.ts - + src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - + + - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/database/repository.ts - - + + +src/core/trpc/procedures/api-config.procedure.ts->src/core/database/index.ts + + - + src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + - + src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - + + - + src/core/utils/package-json.ts->package.json - - + + + + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/typings/docker.ts + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/repository.ts - - + + - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/typings/docker.ts - - + + +src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/index.ts + + - + src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - + + + + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/repository.ts - - + + - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - + + +src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/index.ts + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - + + - + src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - + + - - -src/core/trpc/procedures/logs.procedure.ts->src/core/database/repository.ts - - + + +src/core/trpc/procedures/logs.procedure.ts->src/core/database/index.ts + + - + src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - + + - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/database/repository.ts - - + + +src/core/trpc/procedures/stacks.procedure.ts->src/core/database/index.ts + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - + + - + src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->src/typings/websocket.ts - - + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + src/typings/elysiajs.ts - - -elysiajs.ts + + +elysiajs.ts - + src/core/utils/response-handler.ts->src/typings/elysiajs.ts - - + + + + + +src/core/utils/swagger-readme.ts + + +swagger-readme.ts + + - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - - -src/index.ts->src/core/database/repository.ts - - + + +src/index.ts->src/core/database/index.ts + + - + src/index.ts->src/typings/database.ts - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/core/trpc/index.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + + + + +src/index.ts->src/core/utils/swagger-readme.ts + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/utils.ts - - -utils.ts + + +utils.ts - + src/index.ts->src/routes/utils.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - - -src/middleware/auth.ts->src/core/database/repository.ts - - + + +src/middleware/auth.ts->src/core/database/index.ts + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - - -src/routes/stacks.ts->src/core/database/repository.ts - - + + +src/routes/stacks.ts->src/core/database/index.ts + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + - + src/routes/utils.ts->src/core/utils/package-json.ts - - + + - + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - - -src/routes/api-config.ts->src/core/database/repository.ts - - + + +src/routes/api-config.ts->src/core/database/index.ts + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + + + + +src/routes/docker-manager.ts->src/typings/docker.ts + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - - -src/routes/docker-manager.ts->src/core/database/repository.ts - - + + +src/routes/docker-manager.ts->src/core/database/index.ts + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + + + + +src/routes/docker-stats.ts->src/typings/docker.ts + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - - - - -src/routes/docker-stats.ts->src/core/database/repository.ts - - + + - - -src/routes/docker-stats.ts->src/typings/docker.ts - - + + +src/routes/docker-stats.ts->src/core/database/index.ts + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - - -src/routes/docker-websocket.ts->src/core/database/repository.ts - - + + +src/routes/docker-websocket.ts->src/core/database/index.ts + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - - - - -src/routes/logs.ts->src/core/database/repository.ts - - + + + + + +src/routes/logs.ts->src/core/database/index.ts + + From 84125a96a3d39f71b081bd904bece3f031d7cea5 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 30 Mar 2025 00:01:18 +0100 Subject: [PATCH 229/369] Fix: Delete unused files --- src/core/database/repository.ts | 565 -------------------------------- 1 file changed, 565 deletions(-) delete mode 100644 src/core/database/repository.ts diff --git a/src/core/database/repository.ts b/src/core/database/repository.ts deleted file mode 100644 index 1c920a74..00000000 --- a/src/core/database/repository.ts +++ /dev/null @@ -1,565 +0,0 @@ -import { executeDbOperation } from "./helper"; -import Database from "bun:sqlite"; -import { logger } from "~/core/utils/logger"; -import type { DockerHost, HostStats } from "~/typings/docker"; -import type { config, stacks_config } from "~/typings/database"; - -const db = new Database("dockstatapi.db", { strict: true }); -db.exec("PRAGMA journal_mode = WAL;"); - -export const dbFunctions = { - init() { - const startTime = Date.now(); - db.exec(` - CREATE TABLE IF NOT EXISTS backend_log_entries ( - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - level TEXT NOT NULL, - message TEXT NOT NULL, - file TEXT NOT NULL, - line NUMBER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS stacks_config ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - version INTEGER NOT NULL, - custom BOOLEAN NOT NULL, - source TEXT NOT NULL, - container_count INTEGER NOT NULL, - stack_prefix TEXT NOT NULL, - automatic_reboot_on_error BOOLEAN NOT NULL, - image_updates BOOLEAN NOT NULL - ); - - CREATE TABLE IF NOT EXISTS docker_hosts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - hostAddress TEXT NOT NULL, - secure BOOLEAN NOT NULL - ); - - CREATE TABLE IF NOT EXISTS host_stats ( - hostId INTEGER PRIMARY KEY NOT NULL, - hostName TEXT NOT NULL, - dockerVersion TEXT NOT NULL, - apiVersion TEXT NOT NULL, - os TEXT NOT NULL, - architecture TEXT NOT NULL, - totalMemory INTEGER NOT NULL, - totalCPU INTEGER NOT NULL, - labels TEXT NOT NULL, - containers INTEGER NOT NULL, - containersRunning INTEGER NOT NULL, - containersStopped INTEGER NOT NULL, - containersPaused INTEGER NOT NULL, - images INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS container_stats ( - id TEXT NOT NULL, - hostId TEXT NOT NULL, - name TEXT NOT NULL, - image TEXT NOT NULL, - status TEXT NOT NULL, - state TEXT NOT NULL, - cpu_usage FLOAT NOT NULL, - memory_usage FLOAT NOT NULL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE IF NOT EXISTS config ( - keep_data_for NUMBER NOT NULL, - fetching_interval NUMBER NOT NULL, - api_key TEXT NOT NULL - ); - `); - - logger.info("Starting server..."); - - /* - * Default values: - * - Data retention value for the database (logs and container stats) 7 days - * - Data fetcher for the Database: 5 minutes - * - api_key: changeme - */ - const configRow = db - .prepare(`SELECT COUNT(*) AS count FROM config`) - .get() as { count: number }; - if (configRow.count === 0) { - logger.debug("Initializing default config"); - const stmt = db.prepare( - ` - INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme") - `, - ); - stmt.run(); - } - - const hostRow = db - .prepare(`SELECT COUNT(*) AS count FROM docker_hosts`) - .get("Localhost") as { count: number }; - if (hostRow.count === 0) { - logger.debug("Initializing default docker host (Localhost)"); - const stmt = db.prepare( - ` - INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?) - `, - ); - stmt.run("Localhost", "localhost:2375", false); - } - logger.debug("__task__ __db__ Initializing Database ⏳"); - const duration = Date.now() - startTime; - logger.debug(`__task__ __db__ Initializing Database ✔️ (${duration}ms)`); - }, - - addDockerHost(host: DockerHost) { - return executeDbOperation( - "Add Docker Host", - () => { - const stmt = db.prepare(` - INSERT INTO docker_hosts (name, hostAddress, secure) - VALUES (?, ?, ?) - `); - return stmt.run(host.name, host.hostAddress, host.secure); - }, - () => { - if (host.name.length < 1) { - logger.error("Hostname needed"); - throw new Error("Invalid data provided - Hostname needed"); - } - - if (host.hostAddress.length < 1) { - logger.error("hostAddress needed"); - throw new Error("Invalid data provided - hostAddress needed"); - } - - if ( - typeof host.name !== "string" || - typeof host.secure !== "boolean" || - typeof host.hostAddress !== "string" - ) { - logger.error("Invalid parameter types for addDockerHost"); - throw new TypeError("Invalid parameter types for addDockerHost"); - } - }, - ); - }, - - getDockerHosts(): DockerHost[] { - return executeDbOperation( - "Get Docker Hosts", - () => { - const stmt = db.prepare(` - SELECT id, name, hostAddress, secure - FROM docker_hosts - ORDER BY id DESC - `); - return stmt.all() as DockerHost[]; - }, - () => {}, - ); - }, - - addLogEntry: ( - level: string, - message: string, - file_name: string, - line: number, - ) => { - if ( - typeof level !== "string" || - typeof message !== "string" || - typeof file_name !== "string" || - typeof line !== "number" - ) { - logger.crit("Invalid parameter types for addLogEntry"); - throw new TypeError("Invalid parameter types for addLogEntry"); - } - - const stmt = db.prepare(` - INSERT INTO backend_log_entries (level, message, file, line) - VALUES (?, ?, ?, ?) - `); - return stmt.run(level, message, file_name, line); - }, - - getAllLogs() { - return executeDbOperation( - "Get All Logs", - () => { - const stmt = db.prepare(` - SELECT timestamp, level, message, file, line - FROM backend_log_entries - ORDER BY timestamp DESC - `); - return stmt.all(); - }, - () => {}, - ); - }, - - getLogsByLevel(level: string) { - return executeDbOperation( - "Get Logs By Level", - () => { - const stmt = db.prepare(` - SELECT timestamp, level, message, file, line - FROM backend_log_entries - WHERE level = ? - ORDER BY timestamp DESC - `); - return stmt.all(level); - }, - () => { - if (typeof level !== "string") { - logger.error("Level parameter must be a string"); - throw new TypeError("Level parameter must be a string"); - } - }, - ); - }, - - updateDockerHost(host: DockerHost) { - return executeDbOperation( - "Update Docker Host", - () => { - const stmt = db.prepare(` - UPDATE docker_hosts - SET hostAddress = ?, secure = ?, name = ? - WHERE id = ? - `); - return stmt.run( - host.hostAddress, - host.secure, - host.name, - String(host.id), - ); - }, - () => { - if ( - typeof host.name !== "string" || - typeof host.hostAddress !== "string" || - typeof host.secure !== "boolean" || - typeof host.id !== "number" - ) { - logger.error("Invalid parameter types for updateDockerHost"); - throw new TypeError("Invalid parameter types for updateDockerHost"); - } - }, - ); - }, - - deleteDockerHost(id: number) { - return executeDbOperation( - "Delete Docker Host", - () => { - const stmt = db.prepare(` - DELETE FROM docker_hosts - WHERE id = ? - `); - return stmt.run(id); - }, - () => { - if (typeof id !== "number") { - logger.error("Invalid parameter type for deleteDockerHost"); - throw new TypeError("id parameter must be a number"); - } - }, - ); - }, - - clearAllLogs() { - return executeDbOperation( - "Clear All Logs", - () => { - const stmt = db.prepare(` - DELETE FROM backend_log_entries - `); - return stmt.run(); - }, - () => {}, - ); - }, - - clearLogsByLevel(level: string) { - return executeDbOperation( - "Clear Logs By Level", - () => { - const stmt = db.prepare(` - DELETE FROM backend_log_entries - WHERE level = ? - `); - return stmt.run(level); - }, - () => { - if (typeof level !== "string") { - logger.error("Invalid parameter type for clearLogsByLevel"); - throw new TypeError("Level parameter must be a string"); - } - }, - ); - }, - - updateConfig( - fetching_interval: number, - keep_data_for: number, - api_key: string, - ) { - return executeDbOperation( - "Update Config", - () => { - const stmt = db.prepare(` - UPDATE config - SET fetching_interval = ?, - keep_data_for = ?, - api_key = ? - `); - return stmt.run(fetching_interval, keep_data_for, api_key); - }, - () => { - if ( - typeof fetching_interval !== "number" || - typeof keep_data_for !== "number" - ) { - logger.error("Invalid parameter types for updateConfig"); - throw new TypeError("Invalid parameter types for updateConfig"); - } - }, - ); - }, - - getConfig() { - return executeDbOperation( - "Get Config", - () => { - const stmt = db.prepare(` - SELECT keep_data_for, fetching_interval, api_key - FROM config - `); - return stmt.all(); - }, - () => {}, - ); - }, - - deleteOldData(days: number) { - return executeDbOperation( - "Delete Old Data", - () => { - const deleteContainerStmt = db.prepare(` - DELETE FROM container_stats - WHERE timestamp < datetime('now', '-' || ? || ' days') - `); - deleteContainerStmt.run(days); - - const deleteLogsStmt = db.prepare(` - DELETE FROM backend_log_entries - WHERE timestamp < datetime('now', '-' || ? || ' days') - `); - deleteLogsStmt.run(days); - }, - () => { - if (typeof days !== "number") { - logger.error("Invalid parameter type for deleteOldData"); - throw new TypeError("Days parameter must be a number"); - } - }, - ); - }, - - addContainerStats( - id: string, - hostId: string, - name: string, - image: string, - status: string, - state: string, - cpu_usage: number, - memory_usage: number, - ) { - return executeDbOperation( - "Add Container Stats", - () => { - const stmt = db.prepare(` - INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `); - return stmt.run( - id, - hostId, - name, - image, - status, - state, - cpu_usage, - memory_usage, - ); - }, - () => { - if ( - typeof id !== "string" || - typeof hostId !== "string" || - typeof name !== "string" || - typeof image !== "string" || - typeof status !== "string" || - typeof state !== "string" || - typeof cpu_usage !== "number" || - typeof memory_usage !== "number" - ) { - logger.error("Invalid parameter types for addContainerStats"); - throw new TypeError("Invalid parameter types for addContainerStats"); - } - }, - ); - }, - - updateHostStats(stats: HostStats) { - return executeDbOperation( - "Update Host Stats", - () => { - const labelsJson = JSON.stringify(stats.labels); - const stmt = db.prepare(` - INSERT INTO host_stats ( - hostId, - hostName, - dockerVersion, - apiVersion, - os, - architecture, - totalMemory, - totalCPU, - labels, - containers, - containersRunning, - containersStopped, - containersPaused, - images - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(hostId) DO UPDATE SET - dockerVersion = excluded.dockerVersion, - apiVersion = excluded.apiVersion, - os = excluded.os, - architecture = excluded.architecture, - totalMemory = excluded.totalMemory, - totalCPU = excluded.totalCPU, - labels = excluded.labels, - containers = excluded.containers, - containersRunning = excluded.containersRunning, - containersStopped = excluded.containersStopped, - containersPaused = excluded.containersPaused, - images = excluded.images; - `); - return stmt.run( - stats.hostId, - stats.hostName, - stats.dockerVersion, - stats.apiVersion, - stats.os, - stats.architecture, - stats.totalMemory, - stats.totalCPU, - labelsJson, - stats.containers, - stats.containersRunning, - stats.containersStopped, - stats.containersPaused, - stats.images, - ); - }, - () => {}, - ); - }, - - addStack(stack_config: stacks_config) { - return executeDbOperation( - "Add Stack Config", - () => { - const stmt = db.prepare(` - INSERT INTO stacks_config ( - name, - version, - custom, - source, - container_count, - stack_prefix, - automatic_reboot_on_error, - image_updates - ) - VALUES(?, ?, ?, ?, ?, ?, ?, ?) - `); - return stmt.run( - stack_config.name, - stack_config.version, - stack_config.custom, - stack_config.source, - stack_config.container_count, - stack_config.stack_prefix, - stack_config.automatic_reboot_on_error, - stack_config.image_updates, - ); - }, - () => {}, - ); - }, - - getStacks() { - return executeDbOperation( - "Get Stacks", - () => { - const stmt = db.prepare(` - SELECT name, version, custom, source, container_count, stack_prefix, automatic_reboot_on_error, image_updates - FROM stacks_config - ORDER BY name DESC - `); - return stmt.all(); - }, - () => {}, - ); - }, - - deleteStack(id: number) { - return executeDbOperation( - "Delete Stack", - () => { - const stmt = db.prepare(` - DELETE FROM stacks_config - WHERE id = ?; - `); - return stmt.run(id); - }, - () => {}, - ); - }, - - updateStack(stack_config: stacks_config) { - return executeDbOperation( - "Update Stack", - () => { - const stmt = db.prepare(` - UPDATE stacks_config - SET - version = ?, - custom = ?, - source = ?, - container_count = ?, - stack_prefix = ?, - automatic_reboot_on_error = ?, - image_updates = ? - WHERE name = ?; - `); - return stmt.run( - stack_config.version, - stack_config.custom, - stack_config.source, - stack_config.container_count, - stack_config.stack_prefix, - stack_config.automatic_reboot_on_error, - stack_config.image_updates, - stack_config.name, - ); - }, - () => {}, - ); - }, -}; From 15d7d0609c57935186ba0f050348464e0289a10d Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 30 Mar 2025 00:03:04 +0100 Subject: [PATCH 230/369] Fix: Remove unused file and update dependencies --- bun.lock | 21 ++++++++++++++++----- package.json | 9 +++++---- src/core/utils/helpers.ts | 15 --------------- 3 files changed, 21 insertions(+), 24 deletions(-) delete mode 100644 src/core/utils/helpers.ts diff --git a/bun.lock b/bun.lock index b8dfca8e..afc95230 100644 --- a/bun.lock +++ b/bun.lock @@ -24,6 +24,7 @@ "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", + "logform": "^2.7.0", "typescript": "^5.8.2", "wrap-ansi": "^9.0.0", "zod": "^3.24.2", @@ -94,9 +95,9 @@ "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], - "@types/dockerode": ["@types/dockerode@3.3.35", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-P+DCMASlsH+QaKkDpekKrP5pLls767PPs+/LrlVbKnEnY5tMpEUa2C6U4gRsdFZengOqxdCIqy16R22Q3pLB6Q=="], + "@types/dockerode": ["@types/dockerode@3.3.36", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-K0wTBKjjVI1xS4zeLynssmmbpPl4AnWZ/MJ3JBTi9eGzEmu+xgMLVSKiWzsy/z+3GBPLD5+uE/i/6ZTeZPaX7A=="], - "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + "@types/node": ["@types/node@22.13.14", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w=="], "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], @@ -132,7 +133,7 @@ "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], - "bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="], + "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], @@ -236,7 +237,7 @@ "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - "knip": ["knip@5.46.0", "", { "dependencies": { "@nodelib/fs.walk": "3.0.1", "@snyk/github-codeowners": "1.1.0", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.0", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "summary": "2.1.0", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-WedHSK5xNBWYgm64Rt5B9b0CVXL2kRBcyCeet3NHgdv9en3QE4AWSDPEiX48NoPUBW3h//9S0VwLF5MG/MPi3g=="], + "knip": ["knip@5.46.3", "", { "dependencies": { "@nodelib/fs.walk": "3.0.1", "@snyk/github-codeowners": "1.1.0", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.0", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "summary": "2.1.0", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-DpxZYvFDh0POjgnfXie39zd4SCxmw3iQTSLPgnf1Umq+k+sCHjcv553UmI3hfo39qlVIq2c8XSsjS3IeZfdAoA=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], @@ -364,7 +365,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], + "yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -380,8 +381,14 @@ "@scalar/themes/@scalar/types": ["@scalar/types@0.1.2", "", { "dependencies": { "@scalar/openapi-types": "0.1.9", "@unhead/schema": "^1.11.11", "zod": "^3.23.8" } }, "sha512-5kCLQRwAYWt1ds110EaUb9yonc3KoQYNyo4YUCigJLOnoNugbqkEX0zRudGevItiuk+xg4uOYd30r3C+6xAasA=="], + "@types/docker-modem/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + + "@types/split2/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + "@types/ssh2/@types/node": ["@types/node@18.19.80", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ=="], + "@types/ws/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -392,12 +399,16 @@ "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + "docker-compose/yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], + "easy-table/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "fast-glob/@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "protobufjs/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="], diff --git a/package.json b/package.json index b024205c..af982790 100644 --- a/package.json +++ b/package.json @@ -28,20 +28,21 @@ "@elysiajs/trpc": "^1.1.0", "@trpc/server": "^10.45.2", "chalk": "^5.4.1", - "docker-compose": "^1.1.1", + "docker-compose": "^1.2.0", "dockerode": "^4.0.4", "elysia": "latest", "knip": "latest", "split2": "^4.2.0", "winston": "^3.17.0", - "yaml": "^2.7.0" + "yaml": "^2.7.1" }, "devDependencies": { - "@types/dockerode": "^3.3.34", - "@types/node": "^22.13.10", + "@types/dockerode": "^3.3.36", + "@types/node": "^22.13.14", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", + "logform": "^2.7.0", "typescript": "^5.8.2", "wrap-ansi": "^9.0.0", "zod": "^3.24.2" diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts deleted file mode 100644 index 1bc02063..00000000 --- a/src/core/utils/helpers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { logger } from "./logger"; - -export function findObjectByKey( - array: T[], - key: keyof T, - value: T[keyof T] -): T | undefined { - const data = array.find((item) => item[key] === value); - logger.debug( - `Searching ${String(key)} = ${String(value)} in ${String( - JSON.stringify(array) - )} Found Item ${JSON.stringify(data)}` - ); - return data; -} From b2e7ebc0b1d20d26a01fc6345cb2a5a56d1a6888 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 30 Mar 2025 00:04:47 +0100 Subject: [PATCH 231/369] Fix: Remove unused files, update dependencies --- bun.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index afc95230..9a876555 100644 --- a/bun.lock +++ b/bun.lock @@ -10,17 +10,17 @@ "@elysiajs/trpc": "^1.1.0", "@trpc/server": "^10.45.2", "chalk": "^5.4.1", - "docker-compose": "^1.1.1", + "docker-compose": "^1.2.0", "dockerode": "^4.0.4", "elysia": "latest", "knip": "latest", "split2": "^4.2.0", "winston": "^3.17.0", - "yaml": "^2.7.0", + "yaml": "^2.7.1", }, "devDependencies": { - "@types/dockerode": "^3.3.34", - "@types/node": "^22.13.10", + "@types/dockerode": "^3.3.36", + "@types/node": "^22.13.14", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", From 1ec78f660439b2483f6e2c8ee69714019f7f30e0 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 30 Mar 2025 18:54:25 +0200 Subject: [PATCH 232/369] Fix: Removing tRPC since it is not used in DockStat --- bun.lock | 7 - package.json | 5 +- src/core/trpc/README.md | 1 - src/core/trpc/index.ts | 4 - .../trpc/procedures/api-config.procedure.ts | 80 ------- .../procedures/docker-manager.procedure.ts | 65 ------ .../trpc/procedures/docker-stats.procedure.ts | 147 ------------- src/core/trpc/procedures/logs.procedure.ts | 73 ------- src/core/trpc/procedures/stacks.procedure.ts | 206 ------------------ src/core/trpc/router.ts | 19 -- src/core/trpc/trpc.ts | 5 - src/index.ts | 2 - 12 files changed, 1 insertion(+), 613 deletions(-) delete mode 100644 src/core/trpc/README.md delete mode 100644 src/core/trpc/index.ts delete mode 100644 src/core/trpc/procedures/api-config.procedure.ts delete mode 100644 src/core/trpc/procedures/docker-manager.procedure.ts delete mode 100644 src/core/trpc/procedures/docker-stats.procedure.ts delete mode 100644 src/core/trpc/procedures/logs.procedure.ts delete mode 100644 src/core/trpc/procedures/stacks.procedure.ts delete mode 100644 src/core/trpc/router.ts delete mode 100644 src/core/trpc/trpc.ts diff --git a/bun.lock b/bun.lock index 9a876555..ca73cdb5 100644 --- a/bun.lock +++ b/bun.lock @@ -7,8 +7,6 @@ "@elysiajs/server-timing": "^1.2.1", "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", - "@elysiajs/trpc": "^1.1.0", - "@trpc/server": "^10.45.2", "chalk": "^5.4.1", "docker-compose": "^1.2.0", "dockerode": "^4.0.4", @@ -27,7 +25,6 @@ "logform": "^2.7.0", "typescript": "^5.8.2", "wrap-ansi": "^9.0.0", - "zod": "^3.24.2", }, }, }, @@ -47,8 +44,6 @@ "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], - "@elysiajs/trpc": ["@elysiajs/trpc@1.1.0", "", { "peerDependencies": { "elysia": ">= 1.1.0" } }, "sha512-M8QWC+Wa5Z5MWY/+uMQuwZ+JoQkp4jOc1ra4SncFy1zSjFGin59LO1AT0pE+DRJaFV17gha9y7cB6Q7GnaJEAw=="], - "@grpc/grpc-js": ["@grpc/grpc-js@1.13.0", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-pMuxInZjUnUkgMT2QLZclRqwk2ykJbIU05aZgPgJYXEpN9+2I7z7aNwcjWZSycRPl232FfhPszyBFJyOxTHNog=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], @@ -91,8 +86,6 @@ "@snyk/github-codeowners": ["@snyk/github-codeowners@1.1.0", "", { "dependencies": { "commander": "^4.1.1", "ignore": "^5.1.8", "p-map": "^4.0.0" }, "bin": { "github-codeowners": "dist/cli.js" } }, "sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw=="], - "@trpc/server": ["@trpc/server@10.45.2", "", {}, "sha512-wOrSThNNE4HUnuhJG6PfDRp4L2009KDVxsd+2VYH8ro6o/7/jwYZ8Uu5j+VaW+mOmc8EHerHzGcdbGNQSAUPgg=="], - "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], "@types/dockerode": ["@types/dockerode@3.3.36", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-K0wTBKjjVI1xS4zeLynssmmbpPl4AnWZ/MJ3JBTi9eGzEmu+xgMLVSKiWzsy/z+3GBPLD5+uE/i/6ZTeZPaX7A=="], diff --git a/package.json b/package.json index af982790..620e6889 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,6 @@ "@elysiajs/server-timing": "^1.2.1", "@elysiajs/static": "^1.2.0", "@elysiajs/swagger": "^1.2.2", - "@elysiajs/trpc": "^1.1.0", - "@trpc/server": "^10.45.2", "chalk": "^5.4.1", "docker-compose": "^1.2.0", "dockerode": "^4.0.4", @@ -44,8 +42,7 @@ "cross-env": "^7.0.3", "logform": "^2.7.0", "typescript": "^5.8.2", - "wrap-ansi": "^9.0.0", - "zod": "^3.24.2" + "wrap-ansi": "^9.0.0" }, "module": "src/index.js", "trustedDependencies": [ diff --git a/src/core/trpc/README.md b/src/core/trpc/README.md deleted file mode 100644 index 32bdb3f4..00000000 --- a/src/core/trpc/README.md +++ /dev/null @@ -1 +0,0 @@ -Please see: [DockStatAPI tRPC Routes Reference](https://outline.itsnik.de/s/dockstat/doc/trpc-2hzqJ7BvA0) diff --git a/src/core/trpc/index.ts b/src/core/trpc/index.ts deleted file mode 100644 index 7a13655b..00000000 --- a/src/core/trpc/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { trpc } from "@elysiajs/trpc"; -import { appRouter } from "./router"; - -export default trpc(appRouter, { endpoint: "/trpc" }); diff --git a/src/core/trpc/procedures/api-config.procedure.ts b/src/core/trpc/procedures/api-config.procedure.ts deleted file mode 100644 index f5175684..00000000 --- a/src/core/trpc/procedures/api-config.procedure.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { dbFunctions } from "~/core/database"; -import { logger } from "~/core/utils/logger"; -import { - version, - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, -} from "~/core/utils/package-json"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { router, publicProcedure } from "../trpc"; -import { config } from "~/typings/database"; - -const configInputSchema = z.object({ - fetching_interval: z.number(), - keep_data_for: z.number(), - api_key: z.string(), -}); - -export const configProcedure = router({ - get: publicProcedure.query(() => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - logger.debug("tRPC: Fetched backend config"); - return distinct; - } catch (error) { - logger.error("tRPC config get error", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Error getting the DockStatAPI config", - cause: error, - }); - } - }), - - update: publicProcedure.input(configInputSchema).mutation(({ input }) => { - try { - const { fetching_interval, keep_data_for, api_key } = input; - dbFunctions.updateConfig(fetching_interval, keep_data_for, api_key); - return { success: true, message: "Updated DockStatAPI config" }; - } catch (error) { - logger.error("tRPC config update error", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Error updating the DockStatAPI config", - cause: error, - }); - } - }), - - package: publicProcedure.query(() => { - try { - logger.debug("tRPC: Fetching package.json"); - return { - version, - description, - license, - authorName, - authorEmail, - authorWebsite, - contributors, - dependencies, - devDependencies, - }; - } catch (error) { - logger.error("tRPC package info error", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Error while reading package.json", - cause: error, - }); - } - }), -}); diff --git a/src/core/trpc/procedures/docker-manager.procedure.ts b/src/core/trpc/procedures/docker-manager.procedure.ts deleted file mode 100644 index a91e7691..00000000 --- a/src/core/trpc/procedures/docker-manager.procedure.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { dbFunctions } from "~/core/database"; -import { logger } from "~/core/utils/logger"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { router, publicProcedure } from "../trpc"; -import { DockerHost } from "~/typings/docker"; - -const addHostInput = z.object({ - name: z.string(), - hostAddress: z.string(), - secure: z.boolean(), -}); - -const updateHostInput = z.object({ - name: z.string(), - hostAddress: z.string(), - secure: z.boolean(), - id: z.number(), -}); - -export const dockerManagerProcedure = router({ - addHost: publicProcedure.input(addHostInput).mutation(({ input }) => { - try { - dbFunctions.addDockerHost(input as DockerHost); - logger.debug(`Added docker host (${input.name})`); - return { success: true, message: `Added docker host (${input.name})` }; - } catch (error) { - logger.error("Error adding docker host", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Error adding docker host", - cause: error, - }); - } - }), - - updateHost: publicProcedure.input(updateHostInput).mutation(({ input }) => { - try { - dbFunctions.updateDockerHost(input); - return { success: true, message: `Updated docker host (${name})` }; - } catch (error) { - logger.error("Error updating docker host", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to update host", - cause: error, - }); - } - }), - - getHosts: publicProcedure.query(() => { - try { - const dockerHosts = dbFunctions.getDockerHosts(); - logger.debug("Retrieved docker hosts via tRPC"); - return dockerHosts; - } catch (error) { - logger.error("Error retrieving docker hosts", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to retrieve hosts", - cause: error, - }); - } - }), -}); diff --git a/src/core/trpc/procedures/docker-stats.procedure.ts b/src/core/trpc/procedures/docker-stats.procedure.ts deleted file mode 100644 index 0a289293..00000000 --- a/src/core/trpc/procedures/docker-stats.procedure.ts +++ /dev/null @@ -1,147 +0,0 @@ -import Docker from "dockerode"; -import { dbFunctions } from "~/core/database"; -import { getDockerClient } from "~/core/docker/client"; -import { - calculateCpuPercent, - calculateMemoryUsage, -} from "~/core/utils/calculations"; -import { logger } from "~/core/utils/logger"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { router, publicProcedure } from "../trpc"; -import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; -import type { DockerInfo } from "~/typings/dockerode"; - -export const dockerStatsProcedure = router({ - getContainers: publicProcedure.query(async () => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; - - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Docker host connection failed", - cause: pingError, - }); - } - - const hostContainers = await docker.listContainers({ all: true }); - - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - reject( - new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Error fetching container stats", - cause: error, - }), - ); - } - if (!stats) { - reject( - new TRPCError({ - code: "NOT_FOUND", - message: "No stats available", - }), - ); - } - resolve(stats as Docker.ContainerStats); - }); - }, - ); - - containers.push({ - id: containerInfo.Id, - hostId: host.name, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - }); - } catch (containerError) { - logger.error( - "Error fetching container stats", - containerError, - ); - } - }), - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (hostError) { - logger.error("Error fetching containers for host", hostError); - } - }), - ); - - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - logger.error("Error fetching containers", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to retrieve containers", - cause: error, - }); - } - }), - - getHostStats: publicProcedure - .input(z.object({ id: z.string() })) - .query(async ({ input }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = hosts.find((h) => h.name === input.id); - - if (!host) { - throw new TRPCError({ - code: "NOT_FOUND", - message: `Host (${input.id}) not found`, - }); - } - - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - logger.error("Error fetching host stats", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to retrieve host config", - cause: error, - }); - } - }), -}); diff --git a/src/core/trpc/procedures/logs.procedure.ts b/src/core/trpc/procedures/logs.procedure.ts deleted file mode 100644 index b15fc9f7..00000000 --- a/src/core/trpc/procedures/logs.procedure.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { dbFunctions } from "~/core/database"; -import { logger } from "~/core/utils/logger"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { router, publicProcedure } from "../trpc"; - -const logLevelSchema = z.enum(["debug", "info", "warn", "error"]); - -export const logsProcedure = router({ - getAll: publicProcedure.query(() => { - try { - const logs = dbFunctions.getAllLogs(); - logger.debug("Retrieved all logs via tRPC"); - return logs; - } catch (error) { - logger.error("Failed to retrieve logs", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to retrieve logs", - cause: error, - }); - } - }), - - getByLevel: publicProcedure - .input(z.object({ level: logLevelSchema })) - .query(({ input }) => { - try { - const logs = dbFunctions.getLogsByLevel(input.level); - logger.debug(`Retrieved logs (level: ${input.level}) via tRPC`); - return logs; - } catch (error) { - logger.error("Failed to retrieve logs by level", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to retrieve logs by level", - cause: error, - }); - } - }), - - clearAll: publicProcedure.mutation(() => { - try { - dbFunctions.clearAllLogs(); - logger.debug("Cleared all logs via tRPC"); - return { success: true }; - } catch (error) { - logger.error("Failed to clear all logs", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Could not delete all logs", - cause: error, - }); - } - }), - - clearByLevel: publicProcedure - .input(z.object({ level: logLevelSchema })) - .mutation(({ input }) => { - try { - dbFunctions.clearLogsByLevel(input.level); - logger.debug(`Cleared logs (level: ${input.level}) via tRPC`); - return { success: true }; - } catch (error) { - logger.error("Failed to clear logs by level", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Could not clear logs by level", - cause: error, - }); - } - }), -}); diff --git a/src/core/trpc/procedures/stacks.procedure.ts b/src/core/trpc/procedures/stacks.procedure.ts deleted file mode 100644 index 495fd32d..00000000 --- a/src/core/trpc/procedures/stacks.procedure.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { dbFunctions } from "~/core/database"; -import { logger } from "~/core/utils/logger"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { router, publicProcedure } from "../trpc"; -import { - deployStack, - stopStack, - pullStackImages, - restartStack, - getStackStatus, - startStack, -} from "~/core/stacks/controller"; - -const deployStackInput = z.object({ - compose_spec: z.any(), - name: z.string(), - version: z.number(), - automatic_reboot_on_error: z.boolean(), - isCustom: z.boolean().optional(), - image_updates: z.boolean().optional(), - source: z.string(), - stack_prefix: z.string().optional(), -}); - -const stackOperationInput = z.object({ - stack: z.any(), -}); - -const stackStatusInput = z.object({ - stack_name: z.any(), -}); - -export const stacksProcedure = router({ - deploy: publicProcedure - .input(deployStackInput) - .mutation(async ({ input }) => { - try { - const missingParams = []; - if (!input.compose_spec) { - missingParams.push("compose_spec"); - } - if (!input.automatic_reboot_on_error) { - missingParams.push("automatic_reboot_on_error"); - } - if (!input.source) { - missingParams.push("source"); - } - if (!input.name) { - missingParams.push("name"); - } - - if (missingParams.length > 0) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `Missing values: ${missingParams.join(", ")}`, - }); - } - - await deployStack( - input.compose_spec, - input.name, - input.version, - input.source, - input.automatic_reboot_on_error, - input.isCustom || false, - input.image_updates || false, - input.stack_prefix, - ); - - logger.info(`Deployed Stack (${input.name}) via tRPC`); - return { - success: true, - message: `Stack ${input.name} deployed successfully`, - }; - } catch (error) { - logger.error("Error deploying stack", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error ? error.message : "Error deploying stack", - cause: error, - }); - } - }), - - start: publicProcedure - .input(stackOperationInput) - .mutation(async ({ input }) => { - try { - await startStack(input.stack); - logger.info(`Started Stack (${input.stack}) via tRPC`); - return { - success: true, - message: `Stack ${input.stack} started successfully`, - }; - } catch (error) { - logger.error("Error starting stack", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error ? error.message : "Error starting stack", - cause: error, - }); - } - }), - - stop: publicProcedure - .input(stackOperationInput) - .mutation(async ({ input }) => { - try { - await stopStack(input.stack); - logger.info(`Stopped Stack (${input.stack}) via tRPC`); - return { - success: true, - message: `Stack ${input.stack} stopped successfully`, - }; - } catch (error) { - logger.error("Error stopping stack", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error ? error.message : "Error stopping stack", - cause: error, - }); - } - }), - - restart: publicProcedure - .input(stackOperationInput) - .mutation(async ({ input }) => { - try { - await restartStack(input.stack); - logger.info(`Restarted Stack (${input.stack}) via tRPC`); - return { - success: true, - message: `Stack ${input.stack} restarted successfully`, - }; - } catch (error) { - logger.error("Error restarting stack", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error ? error.message : "Error restarting stack", - cause: error, - }); - } - }), - - pullImages: publicProcedure - .input(stackOperationInput) - .mutation(async ({ input }) => { - try { - await pullStackImages(input.stack); - logger.info(`Pulled Stack images (${input.stack}) via tRPC`); - return { - success: true, - message: `Images for stack ${input.stack} pulled successfully`, - }; - } catch (error) { - logger.error("Error pulling images", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error ? error.message : "Error pulling images", - cause: error, - }); - } - }), - - getStatus: publicProcedure - .input(stackStatusInput) - .query(async ({ input }) => { - try { - const status = await getStackStatus(input.stack_name); - logger.info(`Fetched Stack status (${input.stack_name}) via tRPC`); - return { status }; - } catch (error) { - logger.error("Error getting stack status", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error - ? error.message - : "Error getting stack status", - cause: error, - }); - } - }), - - getAll: publicProcedure.query(() => { - try { - const stacks = dbFunctions.getStacks(); - logger.info("Fetched Stacks via tRPC"); - return stacks; - } catch (error) { - logger.error("Error getting stacks", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - error instanceof Error ? error.message : "Error getting stacks", - cause: error, - }); - } - }), -}); diff --git a/src/core/trpc/router.ts b/src/core/trpc/router.ts deleted file mode 100644 index 9e0bddfc..00000000 --- a/src/core/trpc/router.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { router, t } from "./trpc"; -import { configProcedure } from "./procedures/api-config.procedure"; -import { dockerManagerProcedure } from "./procedures/docker-manager.procedure"; -import { dockerStatsProcedure } from "./procedures/docker-stats.procedure"; -import { logsProcedure } from "./procedures/logs.procedure"; -import { stacksProcedure } from "./procedures/stacks.procedure"; - -export const appRouter = router({ - config: configProcedure, - docker: router({ - manager: dockerManagerProcedure, - stats: dockerStatsProcedure, - }), - logs: logsProcedure, - stacks: stacksProcedure, - health: router({ - check: t.procedure.query(() => ({ status: "healthy" })), - }), -}); diff --git a/src/core/trpc/trpc.ts b/src/core/trpc/trpc.ts deleted file mode 100644 index c7813f91..00000000 --- a/src/core/trpc/trpc.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { initTRPC } from "@trpc/server"; - -export const t = initTRPC.create(); -export const { router } = t; -export const publicProcedure = t.procedure; diff --git a/src/index.ts b/src/index.ts index 5cc5af38..16cd5eeb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,6 @@ import { apiConfigRoutes } from "~/routes/api-config"; import { setSchedules } from "~/core/docker/scheduler"; import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; -import trpcRouter from "~/core/trpc"; import { config } from "./typings/database"; import { validateApiKey } from "./middleware/auth"; import { monitorDockerEvents } from "./core/docker/monitor"; @@ -88,7 +87,6 @@ export const DockStatAPI = new Elysia() return { error: validation.error }; } }) - .use(trpcRouter) .use(dockerRoutes) .use(dockerStatsRoutes) .use(backendLogs) From 28760486ada9658bdc43ff2b98b153c2e8c8c82b Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 30 Mar 2025 16:54:59 +0000 Subject: [PATCH 233/369] Update dependency graphs --- dependency-graph.dot | 39 -- dependency-graph.mmd | 117 ++-- dependency-graph.svg | 1286 +++++++++++++++++------------------------- 3 files changed, 546 insertions(+), 896 deletions(-) diff --git a/dependency-graph.dot b/dependency-graph.dot index a3cdc90c..7c5bbef1 100644 --- a/dependency-graph.dot +++ b/dependency-graph.dot @@ -91,44 +91,6 @@ strict digraph "dependency-cruiser output"{ "src/core/stacks/controller.ts" -> "src/typings/docker-compose.ts" [arrowhead="onormal" penwidth="1.0"] "src/core/stacks/controller.ts" -> "bun" "src/core/stacks/controller.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/index.ts" [label= tooltip="index.ts" URL="src/core/trpc/index.ts" fillcolor="#ddfeff"] } } } - "src/core/trpc/index.ts" -> "src/core/trpc/router.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/api-config.procedure.ts" [label= tooltip="api-config.procedure.ts" URL="src/core/trpc/procedures/api-config.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/database/index.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/logger.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/core/utils/package-json.ts" - "src/core/trpc/procedures/api-config.procedure.ts" -> "src/typings/database.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-manager.procedure.ts" [label= tooltip="docker-manager.procedure.ts" URL="src/core/trpc/procedures/docker-manager.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/database/index.ts" - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/core/utils/logger.ts" - "src/core/trpc/procedures/docker-manager.procedure.ts" -> "src/typings/docker.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/docker-stats.procedure.ts" [label= tooltip="docker-stats.procedure.ts" URL="src/core/trpc/procedures/docker-stats.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/database/index.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/docker/client.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/calculations.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/core/utils/logger.ts" - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/trpc/procedures/docker-stats.procedure.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/logs.procedure.ts" [label= tooltip="logs.procedure.ts" URL="src/core/trpc/procedures/logs.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/database/index.ts" - "src/core/trpc/procedures/logs.procedure.ts" -> "src/core/utils/logger.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" subgraph "cluster_src/core/trpc/procedures" {label="procedures" "src/core/trpc/procedures/stacks.procedure.ts" [label= tooltip="stacks.procedure.ts" URL="src/core/trpc/procedures/stacks.procedure.ts" fillcolor="#ddfeff"] } } } } - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/trpc/trpc.ts" - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/database/index.ts" - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/stacks/controller.ts" - "src/core/trpc/procedures/stacks.procedure.ts" -> "src/core/utils/logger.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/router.ts" [label= tooltip="router.ts" URL="src/core/trpc/router.ts" fillcolor="#ddfeff"] } } } - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/api-config.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-manager.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/docker-stats.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/logs.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/procedures/stacks.procedure.ts" - "src/core/trpc/router.ts" -> "src/core/trpc/trpc.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/trpc" {label="trpc" "src/core/trpc/trpc.ts" [label= tooltip="trpc.ts" URL="src/core/trpc/trpc.ts" fillcolor="#ddfeff"] } } } subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/calculations.ts" [label= tooltip="calculations.ts" URL="src/core/utils/calculations.ts" fillcolor="#ddfeff"] } } } subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/change-me-checker.ts" [label= tooltip="change-me-checker.ts" URL="src/core/utils/change-me-checker.ts" fillcolor="#ddfeff"] } } } "src/core/utils/change-me-checker.ts" -> "src/core/utils/logger.ts" @@ -155,7 +117,6 @@ strict digraph "dependency-cruiser output"{ "src/index.ts" -> "src/core/database/index.ts" "src/index.ts" -> "src/core/docker/scheduler.ts" "src/index.ts" -> "src/core/plugins/loader.ts" - "src/index.ts" -> "src/core/trpc/index.ts" "src/index.ts" -> "src/core/utils/logger.ts" "src/index.ts" -> "src/routes/api-config.ts" "src/index.ts" -> "src/routes/docker-manager.ts" diff --git a/dependency-graph.mmd b/dependency-graph.mmd index bbe9dd96..79af901b 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -42,18 +42,6 @@ end subgraph 11["stacks"] 12["controller.ts"] end -subgraph 1G["trpc"] -1H["index.ts"] -1I["router.ts"] -subgraph 1J["procedures"] -1K["api-config.procedure.ts"] -1M["docker-manager.procedure.ts"] -1N["docker-stats.procedure.ts"] -1O["logs.procedure.ts"] -1P["stacks.procedure.ts"] -end -1L["trpc.ts"] -end end subgraph K["typings"] L["docker.ts"] @@ -68,11 +56,11 @@ subgraph S["routes"] T["live-logs.ts"] 10["stacks.ts"] 16["utils.ts"] -1Q["api-config.ts"] -1R["docker-manager.ts"] -1S["docker-stats.ts"] -1T["docker-websocket.ts"] -1V["logs.ts"] +1G["api-config.ts"] +1H["docker-manager.ts"] +1I["docker-stats.ts"] +1J["docker-websocket.ts"] +1L["logs.ts"] end subgraph X["middleware"] Y["auth.ts"] @@ -86,7 +74,7 @@ subgraph 13["fs"] 14["promises"] end 18["package.json"] -1U["stream"] +1K["stream"] 1-->4 1-->W 1-->Y @@ -97,13 +85,12 @@ end 1-->D 1-->19 1-->1E -1-->1H 1-->A -1-->1Q -1-->1R -1-->1S -1-->1T -1-->1V +1-->1G +1-->1H +1-->1I +1-->1J +1-->1L 4-->7 4-->D 4-->V @@ -190,60 +177,30 @@ Y-->Z 1E-->B 1F-->A 1F-->14 -1H-->1I -1I-->1K -1I-->1M -1I-->1N -1I-->1O -1I-->1P -1I-->1L -1K-->1L -1K-->D -1K-->A -1K-->17 -1K-->Q -1M-->1L -1M-->D -1M-->A -1M-->L -1N-->1L -1N-->D -1N-->V -1N-->1D -1N-->A -1N-->L -1N-->1B -1O-->1L -1O-->D -1O-->A -1P-->1L -1P-->D -1P-->12 -1P-->A -1Q-->D -1Q-->7 -1Q-->A -1Q-->17 -1Q-->15 -1Q-->Y -1Q-->Q -1R-->D -1R-->A -1R-->15 -1R-->L -1S-->D -1S-->V -1S-->1D -1S-->A -1S-->15 -1S-->L -1S-->1B -1T-->D -1T-->V -1T-->1D -1T-->A -1T-->15 -1T-->1U -1V-->D -1V-->A +1G-->D +1G-->7 +1G-->A +1G-->17 +1G-->15 +1G-->Y +1G-->Q +1H-->D +1H-->A +1H-->15 +1H-->L +1I-->D +1I-->V +1I-->1D +1I-->A +1I-->15 +1I-->L +1I-->1B +1J-->D +1J-->V +1J-->1D +1J-->A +1J-->15 +1J-->1K +1L-->D +1L-->A diff --git a/dependency-graph.svg b/dependency-graph.svg index c1e4807d..66611d2d 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,82 +4,72 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks -cluster_src/core/trpc - -trpc - - -cluster_src/core/trpc/procedures - -procedures - - cluster_src/core/utils - -utils + +utils - + cluster_src/middleware - -middleware + +middleware - + cluster_src/routes - -routes + +routes - + cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -87,8 +77,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -96,8 +86,8 @@ events - -events + +events @@ -105,8 +95,8 @@ fs - -fs + +fs @@ -114,8 +104,8 @@ fs/promises - -promises + +promises @@ -123,8 +113,8 @@ package.json - -package.json + +package.json @@ -132,8 +122,8 @@ path - -path + +path @@ -141,8 +131,8 @@ src/core/database/config.ts - -config.ts + +config.ts @@ -150,1413 +140,1155 @@ src/core/database/database.ts - -database.ts + +database.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/helper.ts - -helper.ts + +helper.ts src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/database.ts->bun:sqlite - - + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + src/typings/docker.ts - -docker.ts + +docker.ts src/core/database/dockerHosts.ts->src/typings/docker.ts - - + + - + src/core/utils/logger.ts->path - - + + src/core/database/index.ts - -index.ts + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + src/typings/websocket.ts - -websocket.ts + +websocket.ts - + src/core/utils/logger.ts->src/typings/websocket.ts - - + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts->src/typings/docker.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + src/core/database/logs.ts->src/typings/websocket.ts - - + + src/core/database/stacks.ts->src/core/database/database.ts - - + + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + src/typings/database.ts - -database.ts + +database.ts src/core/database/stacks.ts->src/typings/database.ts - - + + src/typings/docker-compose.ts - -docker-compose.ts + +docker-compose.ts src/core/database/stacks.ts->src/typings/docker-compose.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/typings/docker.ts - - + + src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->bun - - + + src/core/docker/monitor.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->src/core/database/index.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + src/core/plugins/plugin-manager.ts->events - - + + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/typings/plugin.ts - -plugin.ts + +plugin.ts src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + src/core/docker/scheduler.ts->src/typings/database.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + src/typings/dockerode.ts - -dockerode.ts + +dockerode.ts src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts src/core/stacks/controller.ts->bun - - + + src/core/stacks/controller.ts->fs/promises - - + + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts->src/core/database/index.ts - - + + src/core/stacks/controller.ts->src/typings/database.ts - - + + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - - - - -src/core/trpc/index.ts - - -index.ts - - - - - -src/core/trpc/router.ts - - -router.ts - - - - - -src/core/trpc/index.ts->src/core/trpc/router.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts - - -api-config.procedure.ts - - + + - - -src/core/trpc/router.ts->src/core/trpc/procedures/api-config.procedure.ts - - - - - -src/core/trpc/trpc.ts - - -trpc.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts - - -docker-manager.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/docker-manager.procedure.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts - - -docker-stats.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/docker-stats.procedure.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts - - -logs.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/logs.procedure.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts - - -stacks.procedure.ts - - - - - -src/core/trpc/router.ts->src/core/trpc/procedures/stacks.procedure.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/database/index.ts - - - - - -src/core/trpc/procedures/api-config.procedure.ts->src/typings/database.ts - - + + +src/routes/live-logs.ts->src/core/utils/logger.ts + + + + - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/trpc/trpc.ts - - + + +src/routes/live-logs.ts->src/typings/websocket.ts + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - - -src/core/trpc/procedures/api-config.procedure.ts->src/core/utils/package-json.ts - - - - -src/core/utils/package-json.ts->package.json - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/typings/docker.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/database/index.ts - - - - -src/core/trpc/procedures/docker-manager.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/docker.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/database/index.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/docker/client.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/utils/calculations.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/typings/dockerode.ts - - - - - -src/core/trpc/procedures/docker-stats.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts->src/core/database/index.ts - - - - - -src/core/trpc/procedures/logs.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/utils/logger.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/database/index.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/stacks/controller.ts - - - - - -src/core/trpc/procedures/stacks.procedure.ts->src/core/trpc/trpc.ts - - - - - -src/routes/live-logs.ts->src/core/utils/logger.ts - - - - - - - -src/routes/live-logs.ts->src/typings/websocket.ts - - +src/core/utils/package-json.ts->package.json + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + src/typings/elysiajs.ts - - -elysiajs.ts + + +elysiajs.ts - + src/core/utils/response-handler.ts->src/typings/elysiajs.ts - - + + - + src/core/utils/swagger-readme.ts - - -swagger-readme.ts + + +swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->src/typings/database.ts - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - - - - -src/index.ts->src/core/trpc/index.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + - + src/index.ts->src/core/utils/swagger-readme.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/utils.ts - - -utils.ts + + +utils.ts - + src/index.ts->src/routes/utils.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + - + src/routes/utils.ts->src/core/utils/package-json.ts - - + + - + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/typings/docker.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + From a8506c57a072de71793ae51b6bbaf37804912a58 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 30 Mar 2025 19:32:20 +0200 Subject: [PATCH 234/369] Fix: Minor code adjustments --- .github/workflows/dependency-graph.yml | 2 +- .gitignore | 3 ++- src/core/database/dockerHosts.ts | 15 ++++++++++----- src/core/docker/store-host-stats.ts | 3 ++- src/core/stacks/controller.ts | 4 ++-- src/core/utils/helpers.ts | 15 +++++++++++++++ src/routes/docker-stats.ts | 4 ++-- src/tests/post.spec.ts | 6 +++--- src/typings/docker.ts | 2 +- 9 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 src/core/utils/helpers.ts diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 03d23f13..4fad4005 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -38,7 +38,7 @@ jobs: - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: - add: "dependency-graph*" + add: '["dependency-graph.svg", "dependency-graph.mmd"]' message: "Update dependency graphs" committer_name: "GitHub Action" committer_email: "action@github.com" diff --git a/.gitignore b/.gitignore index c61c6830..c7aba1f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.db* /stacks /node_modules -.test \ No newline at end of file +.test +dependency-graph* diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts index bdaf2d1a..c8057030 100644 --- a/src/core/database/dockerHosts.ts +++ b/src/core/database/dockerHosts.ts @@ -29,12 +29,17 @@ export function addDockerHost(host: DockerHost) { } export function getDockerHosts(): DockerHost[] { - return executeDbOperation( - "Get Docker Hosts", - () => stmt.selectAll.all() as DockerHost[], - ); + return executeDbOperation("Get Docker Hosts", () => { + const rows = stmt.selectAll.all() as Array< + Omit & { secure: number } + >; + return rows.map((row) => ({ + ...row, + secure: row.secure === 1, + })); + }); } - +1; export function updateDockerHost(host: DockerHost) { return executeDbOperation( "Update Docker Host", diff --git a/src/core/docker/store-host-stats.ts b/src/core/docker/store-host-stats.ts index 9536bc14..fa7b05d6 100644 --- a/src/core/docker/store-host-stats.ts +++ b/src/core/docker/store-host-stats.ts @@ -3,10 +3,11 @@ import { dbFunctions } from "~/core/database"; import { DockerHost, HostStats } from "~/typings/docker"; import { getDockerClient } from "~/core/docker/client"; import { DockerInfo } from "~/typings/dockerode"; +import { findObjectByKey } from "~/core/utils/helpers"; function getHostByName(hostName: string): DockerHost { const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const foundHost = hosts.find((host) => host.name === hostName); + const foundHost = findObjectByKey(hosts, "name", hostName); if (!foundHost) { throw new Error(`Host ${hostName} not found`); } diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 838532c5..1c85b59b 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -5,12 +5,12 @@ import DockerCompose from "docker-compose"; import type { Stack, ComposeSpec } from "~/typings/docker-compose"; import type { stacks_config } from "~/typings/database"; import { rm } from "node:fs/promises"; -import { ErrorLike } from "bun"; +import { findObjectByKey } from "../utils/helpers"; async function getStackName(stack_id: number): Promise { logger.debug(`Fetching stack name for id ${stack_id}`); const stacks = dbFunctions.getStacks(); - const stack = stacks.find((stack) => Number(stack.id) === Number(stack_id)); + const stack = findObjectByKey(stacks, "id", stack_id); if (!stack) { throw new Error(`Stack with id ${stack_id} not found`); } diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts new file mode 100644 index 00000000..7f3a99ca --- /dev/null +++ b/src/core/utils/helpers.ts @@ -0,0 +1,15 @@ +import { logger } from "./logger"; + +export function findObjectByKey( + array: T[], + key: keyof T, + value: T[keyof T], +): T | undefined { + const data = array.find((item) => item[key] === value); + logger.debug( + `Searching ${String(key)} = ${String(value)} in ${String( + JSON.stringify(array), + )} Found Item ${JSON.stringify(data)}`, + ); + return data; +} diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index a600af3a..b6bdd3f3 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -8,6 +8,7 @@ import { } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; +import { findObjectByKey } from "~/core/utils/helpers"; import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; @@ -112,8 +113,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) async ({ params, set }) => { try { const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = hosts.find((h) => h.name === params.id); - + const host = findObjectByKey(hosts, "name", params.id); if (!host) { return responseHandler.simple_error( set, diff --git a/src/tests/post.spec.ts b/src/tests/post.spec.ts index 040012e3..9ce64e9f 100644 --- a/src/tests/post.spec.ts +++ b/src/tests/post.spec.ts @@ -25,18 +25,18 @@ describe("DockStatAPI (POST)", () => { await runTestCode("/docker-config/update-host", 200, "POST", codeBody); const responseBody: DockerHost[] = [ - { id: 2, name: "test", hostAddress: "127.0.0.1:2375", secure: 0 }, + { id: 2, name: "test", hostAddress: "127.0.0.1:2375", secure: false }, { id: 1, name: "Localhost", hostAddress: "localhost:2375", - secure: 0, + secure: false, }, ]; await runTestResponse( "/docker-config/hosts", JSON.stringify(responseBody), - "GET" + "GET", ); }); diff --git a/src/typings/docker.ts b/src/typings/docker.ts index 0d759bac..8b4f5038 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -1,7 +1,7 @@ interface DockerHost { name: string; hostAddress: string; - secure: boolean | number; + secure: boolean; id: number; } From ad3509e59ecd720106c7d878713d9c18b33537d4 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 30 Mar 2025 17:34:05 +0000 Subject: [PATCH 235/369] Update dependency graphs --- dependency-graph.mmd | 128 ++--- dependency-graph.svg | 1077 ++++++++++++++++++++++-------------------- 2 files changed, 618 insertions(+), 587 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 79af901b..844ace0b 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -12,21 +12,22 @@ subgraph 2["core"] subgraph 3["docker"] 4["monitor.ts"] V["client.ts"] -19["scheduler.ts"] -1A["store-host-stats.ts"] -1C["store-container-stats.ts"] +1A["scheduler.ts"] +1B["store-host-stats.ts"] +1D["store-container-stats.ts"] end subgraph 6["plugins"] 7["plugin-manager.ts"] -1E["loader.ts"] +1F["loader.ts"] end subgraph 9["utils"] A["logger.ts"] W["swagger-readme.ts"] -15["response-handler.ts"] -17["package-json.ts"] -1D["calculations.ts"] -1F["change-me-checker.ts"] +15["helpers.ts"] +16["response-handler.ts"] +18["package-json.ts"] +1E["calculations.ts"] +1G["change-me-checker.ts"] end subgraph C["database"] D["index.ts"] @@ -50,17 +51,17 @@ Q["database.ts"] R["docker-compose.ts"] U["plugin.ts"] Z["elysiajs.ts"] -1B["dockerode.ts"] +1C["dockerode.ts"] end subgraph S["routes"] T["live-logs.ts"] 10["stacks.ts"] -16["utils.ts"] -1G["api-config.ts"] -1H["docker-manager.ts"] -1I["docker-stats.ts"] -1J["docker-websocket.ts"] -1L["logs.ts"] +17["utils.ts"] +1H["api-config.ts"] +1I["docker-manager.ts"] +1J["docker-stats.ts"] +1K["docker-websocket.ts"] +1M["logs.ts"] end subgraph X["middleware"] Y["auth.ts"] @@ -73,24 +74,24 @@ G["bun:sqlite"] subgraph 13["fs"] 14["promises"] end -18["package.json"] -1K["stream"] +19["package.json"] +1L["stream"] 1-->4 1-->W 1-->Y 1-->T 1-->10 -1-->16 +1-->17 1-->Q 1-->D -1-->19 -1-->1E +1-->1A +1-->1F 1-->A -1-->1G 1-->1H 1-->1I 1-->1J -1-->1L +1-->1K +1-->1M 4-->7 4-->D 4-->V @@ -144,63 +145,66 @@ Y-->Z 10-->D 10-->12 10-->A -10-->15 +10-->16 +12-->15 12-->D 12-->A 12-->Q 12-->R -12-->5 12-->14 15-->A -15-->Z -16-->17 -16-->15 +16-->A +16-->Z 17-->18 -19-->D -19-->1A -19-->1C -19-->A -19-->Q +17-->16 +18-->19 1A-->D -1A-->V -1A-->A -1A-->L 1A-->1B -1C-->A -1C-->D -1C-->V -1C-->1D -1E-->1F -1E-->A -1E-->7 -1E-->13 -1E-->B +1A-->1D +1A-->A +1A-->Q +1B-->D +1B-->V +1B-->15 +1B-->A +1B-->L +1B-->1C +1D-->A +1D-->D +1D-->V +1D-->1E +1F-->1G 1F-->A -1F-->14 -1G-->D -1G-->7 +1F-->7 +1F-->13 +1F-->B 1G-->A -1G-->17 -1G-->15 -1G-->Y -1G-->Q +1G-->14 1H-->D +1H-->7 1H-->A -1H-->15 -1H-->L +1H-->18 +1H-->16 +1H-->Y +1H-->Q 1I-->D -1I-->V -1I-->1D 1I-->A -1I-->15 +1I-->16 1I-->L -1I-->1B 1J-->D 1J-->V -1J-->1D -1J-->A +1J-->1E 1J-->15 -1J-->1K -1L-->D -1L-->A +1J-->A +1J-->16 +1J-->L +1J-->1C +1K-->D +1K-->V +1K-->1E +1K-->A +1K-->16 +1K-->1L +1M-->D +1M-->A diff --git a/dependency-graph.svg b/dependency-graph.svg index 66611d2d..0549c66b 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,72 +4,72 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -77,8 +77,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -86,8 +86,8 @@ events - -events + +events @@ -95,8 +95,8 @@ fs - -fs + +fs @@ -104,8 +104,8 @@ fs/promises - -promises + +promises @@ -113,8 +113,8 @@ package.json - -package.json + +package.json @@ -122,8 +122,8 @@ path - -path + +path @@ -131,8 +131,8 @@ src/core/database/config.ts - -config.ts + +config.ts @@ -140,1155 +140,1182 @@ src/core/database/database.ts - -database.ts + +database.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/helper.ts - -helper.ts + +helper.ts src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/database.ts->bun:sqlite - - + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + src/typings/docker.ts - -docker.ts + +docker.ts src/core/database/dockerHosts.ts->src/typings/docker.ts - - + + - + src/core/utils/logger.ts->path - - + + src/core/database/index.ts - -index.ts + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + src/typings/websocket.ts - -websocket.ts + +websocket.ts - + src/core/utils/logger.ts->src/typings/websocket.ts - - + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts->src/typings/docker.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + src/core/database/logs.ts->src/typings/websocket.ts - - + + src/core/database/stacks.ts->src/core/database/database.ts - - + + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + src/typings/database.ts - -database.ts + +database.ts src/core/database/stacks.ts->src/typings/database.ts - - + + src/typings/docker-compose.ts - -docker-compose.ts + +docker-compose.ts src/core/database/stacks.ts->src/typings/docker-compose.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/typings/docker.ts - - + + src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->bun - - + + src/core/docker/monitor.ts->src/typings/docker.ts - - + + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->src/core/database/index.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - + + - + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts - - -plugin.ts + + +plugin.ts - + src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + src/core/docker/scheduler.ts->src/typings/database.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + +src/core/utils/helpers.ts + + +helpers.ts + + + + + +src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts + + + + + src/typings/dockerode.ts - - -dockerode.ts + + +dockerode.ts - + src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + + + + +src/core/utils/helpers.ts->src/core/utils/logger.ts + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts - - -src/core/stacks/controller.ts->bun - - - - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts->src/typings/database.ts - - + + - + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + + + + +src/core/stacks/controller.ts->src/core/utils/helpers.ts + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->src/typings/websocket.ts - - + + - + src/core/utils/package-json.ts - + package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + src/typings/elysiajs.ts - - -elysiajs.ts + + +elysiajs.ts - + src/core/utils/response-handler.ts->src/typings/elysiajs.ts - - + + - + src/core/utils/swagger-readme.ts - - -swagger-readme.ts + + +swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->src/typings/database.ts - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + - + src/index.ts->src/core/utils/swagger-readme.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/utils.ts - - -utils.ts + + +utils.ts - + src/index.ts->src/routes/utils.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - + docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + - + src/routes/utils.ts->src/core/utils/package-json.ts - - + + - + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/typings/docker.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + + + + +src/routes/docker-stats.ts->src/core/utils/helpers.ts + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - + stream - + src/routes/docker-websocket.ts->stream - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + From b3192ec9f324af37dd492c82032ba7f73bd51ee9 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 5 Apr 2025 13:07:15 +0200 Subject: [PATCH 236/369] Feat: More info in the non websocket request --- src/routes/docker-stats.ts | 26 ++++++++++++++------------ src/routes/docker-websocket.ts | 14 +++++++------- src/typings/docker.ts | 5 +++++ 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index b6bdd3f3..9cfe465b 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -30,7 +30,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, pingError as string, - "Docker host connection failed", + "Docker host connection failed" ); } @@ -48,19 +48,19 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) set, reject, "An error occurred", - error, + error ); } if (!stats) { return responseHandler.reject( set, reject, - "No stats available", + "No stats available" ); } resolve(stats); }); - }, + } ); containers.push({ @@ -72,20 +72,22 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) state: containerInfo.State, cpuUsage: calculateCpuPercent(stats), memoryUsage: calculateMemoryUsage(stats), + stats: stats, + info: containerInfo, }); } catch (containerError) { logger.error( "Error fetching container stats,", - containerError, + containerError ); } - }), + }) ); logger.debug(`Fetched stats for ${host.name}`); } catch (hostError) { logger.error("Error fetching containers for host,", hostError); } - }), + }) ); set.headers["Content-Type"] = "application/json"; @@ -95,7 +97,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, error as string, - "Failed to retrieve containers", + "Failed to retrieve containers" ); } }, @@ -105,7 +107,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) description: "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", }, - }, + } ) .get( @@ -117,7 +119,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) if (!host) { return responseHandler.simple_error( set, - `Host (${params.id}) not found`, + `Host (${params.id}) not found` ); } @@ -148,7 +150,7 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) return responseHandler.error( set, error as string, - "Failed to retrieve host config", + "Failed to retrieve host config" ); } }, @@ -158,5 +160,5 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) description: "Provides detailed system metrics and Docker runtime information for specified host", }, - }, + } ); diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 43292e2a..8b26542b 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -40,7 +40,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( await docker.ping(); const containers = await docker.listContainers({ all: true }); logger.debug( - `Found ${containers.length} containers on ${host.name} (id: ${host.id})`, + `Found ${containers.length} containers on ${host.name} (id: ${host.id})` ); for (const containerInfo of containers) { @@ -75,7 +75,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( state: containerInfo.State, cpuUsage: calculateCpuPercent(stats) || 0, memoryUsage: calculateMemoryUsage(stats) || 0, - }), + }) ); } catch (error) { logger.error(`Parse error: ${error}`); @@ -89,7 +89,7 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( hostId: host.name, containerId: containerInfo.Id, error: `Stats stream error: ${error}`, - }), + }) ); }); } @@ -102,9 +102,9 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( { headers: {} }, error as string, "Docker connection failed", - 500, - ), - ), + 500 + ) + ) ); } }, @@ -129,5 +129,5 @@ export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( }); connectionStreams.delete(ws); }, - }, + } ); diff --git a/src/typings/docker.ts b/src/typings/docker.ts index 8b4f5038..8f8844ba 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -1,3 +1,6 @@ +import { ContainerStats } from "dockerode"; +import Docker from "dockerode"; + interface DockerHost { name: string; hostAddress: string; @@ -14,6 +17,8 @@ interface ContainerInfo { state: string; cpuUsage: number; memoryUsage: number; + stats: ContainerStats; + info: Docker.ContainerInfo; } interface HostStats { From 5a72c1ce56b624ba7fcacaf6ddd557c8b312b0bd Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 15 Apr 2025 19:45:06 +0200 Subject: [PATCH 237/369] Feat: Stacks progress websocket route --- .gitignore | 1 + .local-tests/stacks.md | 1 + bun.lock | 84 +++----- package.json | 8 +- src/core/database/stacks.ts | 17 +- src/core/docker/scheduler.ts | 12 +- src/core/stacks/controller.ts | 292 ++++++++++++++++++---------- src/core/utils/calculations.ts | 2 +- src/core/utils/change-me-checker.ts | 4 +- src/core/utils/helpers.ts | 8 +- src/core/utils/logger.ts | 107 ++++------ src/core/utils/package-json.ts | 9 +- src/core/utils/response-handler.ts | 3 +- src/index.ts | 53 +++-- src/middleware/auth.ts | 11 +- src/plugins/example.plugin.ts | 29 +-- src/plugins/telegram.plugin.ts | 5 +- src/routes/api-config.ts | 27 +-- src/routes/docker-manager.ts | 20 +- src/routes/docker-stats.ts | 10 +- src/routes/docker-websocket.ts | 9 +- src/routes/live-logs.ts | 6 +- src/routes/live-stacks.ts | 30 +++ src/routes/logs.ts | 11 +- src/routes/stacks.ts | 51 ++--- src/routes/utils.ts | 7 +- src/tests/cleanup.ts | 3 +- src/tests/delete.spec.ts | 1 + src/tests/gets.spec.ts | 4 +- src/tests/helper.ts | 4 +- src/tests/post.spec.ts | 4 +- src/typings/database.ts | 10 +- src/typings/plugin.ts | 1 - src/typings/websocket.ts | 24 ++- tsconfig.json | 2 +- 35 files changed, 487 insertions(+), 383 deletions(-) create mode 100644 src/routes/live-stacks.ts diff --git a/.gitignore b/.gitignore index c7aba1f4..322656b4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /node_modules .test dependency-graph* +build \ No newline at end of file diff --git a/.local-tests/stacks.md b/.local-tests/stacks.md index 4d9290cb..22a2c514 100644 --- a/.local-tests/stacks.md +++ b/.local-tests/stacks.md @@ -14,6 +14,7 @@ - stack_prefix ### JSON + ```json { "compose_spec": { diff --git a/bun.lock b/bun.lock index ca73cdb5..ffa2cdf1 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,7 @@ "@elysiajs/swagger": "^1.2.2", "chalk": "^5.4.1", "docker-compose": "^1.2.0", - "dockerode": "^4.0.4", + "dockerode": "^4.0.5", "elysia": "latest", "knip": "latest", "split2": "^4.2.0", @@ -17,13 +17,13 @@ "yaml": "^2.7.1", }, "devDependencies": { - "@types/dockerode": "^3.3.36", - "@types/node": "^22.13.14", + "@types/dockerode": "^3.3.38", + "@types/node": "^22.14.1", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", "logform": "^2.7.0", - "typescript": "^5.8.2", + "typescript": "^5.8.3", "wrap-ansi": "^9.0.0", }, }, @@ -44,17 +44,17 @@ "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], - "@grpc/grpc-js": ["@grpc/grpc-js@1.13.0", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-pMuxInZjUnUkgMT2QLZclRqwk2ykJbIU05aZgPgJYXEpN9+2I7z7aNwcjWZSycRPl232FfhPszyBFJyOxTHNog=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.13.3", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@4.0.1", "", { "dependencies": { "@nodelib/fs.stat": "4.0.0", "run-parallel": "^1.2.0" } }, "sha512-vAkI715yhnmiPupY+dq+xenu5Tdf2TBQ66jLvBIcCddtz+5Q8LbMKaf9CIJJreez8fQ8fgaY+RaywQx8RJIWpw=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - "@nodelib/fs.walk": ["@nodelib/fs.walk@3.0.1", "", { "dependencies": { "@nodelib/fs.scandir": "4.0.1", "fastq": "^1.15.0" } }, "sha512-nIh/M6Kh3ZtOmlY00DaUYB4xeeV6F3/ts1l29iwl3/cfyY/OuCfUx+v08zgx8TKPTifXRcjjqVQ4KB2zOYSbyw=="], + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], @@ -78,32 +78,28 @@ "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], - "@scalar/themes": ["@scalar/themes@0.9.80", "", { "dependencies": { "@scalar/types": "0.1.2" } }, "sha512-UZM8pQLpGeBtOdUx6yOcj5SPiWo1LaylUVt8HjCRFQ90zZtwbcIWfUWwWOay5nh7cwSVqY2G9eAyGYcNJB12ew=="], + "@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="], "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], - "@sinclair/typebox": ["@sinclair/typebox@0.34.30", "", {}, "sha512-gFB3BiqjDxEoadW0zn+xyMVb7cLxPCoblVn2C/BKpI41WPYi2d6fwHAlynPNZ5O/Q4WEiujdnJzVtvG/Jc2CBQ=="], - - "@snyk/github-codeowners": ["@snyk/github-codeowners@1.1.0", "", { "dependencies": { "commander": "^4.1.1", "ignore": "^5.1.8", "p-map": "^4.0.0" }, "bin": { "github-codeowners": "dist/cli.js" } }, "sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="], "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], - "@types/dockerode": ["@types/dockerode@3.3.36", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-K0wTBKjjVI1xS4zeLynssmmbpPl4AnWZ/MJ3JBTi9eGzEmu+xgMLVSKiWzsy/z+3GBPLD5+uE/i/6ZTeZPaX7A=="], + "@types/dockerode": ["@types/dockerode@3.3.38", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-nnrcfUe2iR+RyOuz0B4bZgQwD9djQa9ADEjp7OAgBs10pYT0KSCtplJjcmBDJz0qaReX5T7GbE5i4VplvzUHvA=="], - "@types/node": ["@types/node@22.13.14", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w=="], + "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="], "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], - "@types/ssh2": ["@types/ssh2@1.15.4", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-9JTQgVBWSgq6mAen6PVnrAmty1lqgCMvpfN+1Ck5WRUsyMYPa6qd50/vMJ0y1zkGpOEgLzm8m8Dx/Y5vRouLaA=="], + "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], - "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], - "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], - "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -126,14 +122,12 @@ "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], - "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], + "bun-types": ["bun-types@1.2.9", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw=="], "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], @@ -148,8 +142,6 @@ "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], - "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], - "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], @@ -166,7 +158,7 @@ "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], - "dockerode": ["dockerode@4.0.4", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.0.1", "uuid": "^10.0.0" } }, "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w=="], + "dockerode": ["dockerode@4.0.5", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.1.2", "uuid": "^10.0.0" } }, "sha512-ZPmKSr1k1571Mrh7oIBS/j0AqAccoecY2yH420ni5j1KyNMgnoTh4Nu4FWunh0HZIJmRSmSysJjBIpa/zyWUEA=="], "easy-table": ["easy-table@1.2.0", "", { "dependencies": { "ansi-regex": "^5.0.1" }, "optionalDependencies": { "wcwidth": "^1.0.1" } }, "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww=="], @@ -206,10 +198,6 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], @@ -230,7 +218,7 @@ "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - "knip": ["knip@5.46.3", "", { "dependencies": { "@nodelib/fs.walk": "3.0.1", "@snyk/github-codeowners": "1.1.0", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.0", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "summary": "2.1.0", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-DpxZYvFDh0POjgnfXie39zd4SCxmw3iQTSLPgnf1Umq+k+sCHjcv553UmI3hfo39qlVIq2c8XSsjS3IeZfdAoA=="], + "knip": ["knip@5.50.4", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.1", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-In+GjPpd2P3IDZnBBP4QF27vhQOhuBkICiuN9j+DMOf/m/qAFLGcbvuAGxco8IDvf26pvBnfeSmm1f6iNCkgOA=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], @@ -254,6 +242,8 @@ "nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="], + "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], + "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], @@ -262,8 +252,6 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], - "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -276,7 +264,7 @@ "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], - "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + "protobufjs": ["protobufjs@7.5.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-Z2E/kOY1QjoMlCytmexzYfDm/w5fKAiRwpSzGtdnXW1zC88Z2yXazHHrOtwCzn+7wSxyE8PYM4rvVcMphF9sOA=="], "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], @@ -320,11 +308,9 @@ "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="], - "summary": ["summary@2.1.0", "", {}, "sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw=="], - "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], - "tar-fs": ["tar-fs@2.0.1", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.0.0" } }, "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA=="], + "tar-fs": ["tar-fs@2.1.2", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], @@ -336,9 +322,11 @@ "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], - "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + "type-fest": ["type-fest@4.40.0", "", {}, "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -370,17 +358,9 @@ "zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="], - "@nodelib/fs.scandir/@nodelib/fs.stat": ["@nodelib/fs.stat@4.0.0", "", {}, "sha512-ctr6bByzksKRCV0bavi8WoQevU6plSp2IkllIsEqaiKe2mwNNnaluhnRhcsgGZHrrHk57B3lf95MkLMO3STYcg=="], + "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], - "@scalar/themes/@scalar/types": ["@scalar/types@0.1.2", "", { "dependencies": { "@scalar/openapi-types": "0.1.9", "@unhead/schema": "^1.11.11", "zod": "^3.23.8" } }, "sha512-5kCLQRwAYWt1ds110EaUb9yonc3KoQYNyo4YUCigJLOnoNugbqkEX0zRudGevItiuk+xg4uOYd30r3C+6xAasA=="], - - "@types/docker-modem/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], - - "@types/split2/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], - - "@types/ssh2/@types/node": ["@types/node@18.19.80", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ=="], - - "@types/ws/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + "@types/ssh2/@types/node": ["@types/node@18.19.86", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ=="], "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -388,23 +368,15 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - "docker-compose/yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], - "easy-table/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "fast-glob/@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "protobufjs/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], - "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="], + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], @@ -414,8 +386,6 @@ "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "fast-glob/@nodelib/fs.walk/@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/package.json b/package.json index 620e6889..b29b98d6 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@elysiajs/swagger": "^1.2.2", "chalk": "^5.4.1", "docker-compose": "^1.2.0", - "dockerode": "^4.0.4", + "dockerode": "^4.0.5", "elysia": "latest", "knip": "latest", "split2": "^4.2.0", @@ -35,13 +35,13 @@ "yaml": "^2.7.1" }, "devDependencies": { - "@types/dockerode": "^3.3.36", - "@types/node": "^22.13.14", + "@types/dockerode": "^3.3.38", + "@types/node": "^22.14.1", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", "logform": "^2.7.0", - "typescript": "^5.8.2", + "typescript": "^5.8.3", "wrap-ansi": "^9.0.0" }, "module": "src/index.js", diff --git a/src/core/database/stacks.ts b/src/core/database/stacks.ts index 04916252..18aa1160 100644 --- a/src/core/database/stacks.ts +++ b/src/core/database/stacks.ts @@ -2,6 +2,7 @@ import { Stack } from "~/typings/docker-compose"; import { db } from "./database"; import { executeDbOperation } from "./helper"; import type { stacks_config } from "~/typings/database"; +import { findObjectByKey } from "../utils/helpers"; const stmt = { insert: db.prepare(` @@ -26,7 +27,7 @@ const stmt = { }; export function addStack(stack: stacks_config) { - return executeDbOperation("Add Stack", () => + executeDbOperation("Add Stack", () => stmt.insert.run( stack.name, stack.version, @@ -35,14 +36,16 @@ export function addStack(stack: stacks_config) { stack.container_count, stack.stack_prefix, stack.automatic_reboot_on_error, - stack.image_updates, - ), + stack.image_updates + ) ); + + return findObjectByKey(getStacks(), "name", stack.name)?.id; } export function getStacks() { return executeDbOperation("Get Stacks", () => - stmt.selectAll.all(), + stmt.selectAll.all() ) as Stack[]; } @@ -52,7 +55,7 @@ export function deleteStack(id: number) { () => stmt.delete.run(id), () => { if (typeof id !== "number") throw new TypeError("Invalid stack ID"); - }, + } ); } @@ -66,7 +69,7 @@ export function updateStack(stack: stacks_config) { stack.stack_prefix, stack.automatic_reboot_on_error, stack.image_updates, - stack.name, - ), + stack.name + ) ); } diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index 2d7fae1e..d1dd1248 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -2,7 +2,7 @@ import storeContainerData from "~/core/docker/store-container-stats"; import { dbFunctions } from "~/core/database"; import { config } from "~/typings/database"; import { logger } from "~/core/utils/logger"; -import storeHostData from "~/core/docker//store-host-stats"; +import storeHostData from "~/core/docker/store-host-stats"; function convertFromMinToMs(minutes: number): number { return minutes * 60 * 1000; @@ -11,7 +11,7 @@ function convertFromMinToMs(minutes: number): number { async function initialRun( scheduleName: string, scheduleFunction: Promise | void, - isAsync: boolean, + isAsync: boolean ) { try { if (isAsync) { @@ -54,15 +54,15 @@ async function setSchedules() { } logger.info( - `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, + `Scheduling: Fetching container statistics every ${fetching_interval} minutes` ); logger.info( - `Scheduling: Updating host statistics every ${fetching_interval} minutes`, + `Scheduling: Updating host statistics every ${fetching_interval} minutes` ); logger.info( - `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, + `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days` ); // Schedule container data fetching @@ -93,7 +93,7 @@ async function setSchedules() { await initialRun( "dbFunctions.deleteOldData", dbFunctions.deleteOldData(keep_data_for), - false, + false ); setInterval(() => { try { diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 1c85b59b..19bd0824 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -6,6 +6,16 @@ import type { Stack, ComposeSpec } from "~/typings/docker-compose"; import type { stacks_config } from "~/typings/database"; import { rm } from "node:fs/promises"; import { findObjectByKey } from "../utils/helpers"; +import { postToClient } from "~/routes/live-stacks"; + +const wrapProgressCallback = (progressCallback?: (log: string) => void) => { + return progressCallback + ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; +}; async function getStackName(stack_id: number): Promise { logger.debug(`Fetching stack name for id ${stack_id}`); @@ -19,16 +29,44 @@ async function getStackName(stack_id: number): Promise { async function runStackCommand( stack_id: number, - command: (cwd: string) => Promise, - action: string, + command: ( + cwd: string, + progressCallback?: (log: string) => void + ) => Promise, + action: string ): Promise { try { - const stack = { id: stack_id, name: await getStackName(stack_id) }; - const stackPath = await getStackPath(stack as Stack); - return await command(stackPath); + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + + const progressCallback = (log: string) => { + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message: log.trim(), + timestamp: new Date().toISOString(), + }, + }); + }; + + return await command(stackPath, progressCallback); } catch (error: any) { + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: error.message || String(error), + timestamp: new Date().toISOString(), + }, + }); throw new Error( - `Error while ${action} stack "${stack_id}": ${error.message || error}`, + `Error while ${action} stack "${stack_id}": ${error.message || error}` ); } } @@ -54,21 +92,21 @@ export async function deployStack( automatic_reboot_on_error: boolean, isCustom: boolean, image_updates: boolean, - stack_prefix?: string, + stack_prefix?: string ): Promise { + let stackId: number; + try { logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`); - const serviceCount = stack.services ? Object.keys(stack.services).length : 0; - const resolvedPrefix = stack_prefix ?? ""; const stack_config: stacks_config = { id: 0, - name: name, - version: version, + name, + version, source, stack_prefix: resolvedPrefix, automatic_reboot_on_error, @@ -77,137 +115,183 @@ export async function deployStack( image_updates, }; - if (!stack.name) { - logger.debug(`${JSON.stringify(stack)}`); + if (!name) { throw new Error("Stack name needed"); } - dbFunctions.addStack(stack_config); + stackId = dbFunctions.addStack(stack_config) as number; + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }); const stackYaml: Stack = { - name: name, - source: source, - version: version, + id: stackId, + name, + source, + version, compose_spec: stack, }; + await createStackYAML(stackYaml); - const stackPath = await getStackPath(stackYaml); - await DockerCompose.upAll({ cwd: stackPath }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } -} -export async function stopStack(stack_id: number): Promise { - try { await runStackCommand( - stack_id, - (cwd) => DockerCompose.downAll({ cwd }), - "stopping", + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying" ); + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id: 0, + action: "deploying", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); throw new Error(errorMsg); } } +export async function stopStack(stack_id: number): Promise { + // Note the await to discard the result (convert to void) + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.downAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping" + ); +} + export async function startStack(stack_id: number): Promise { - try { - await runStackCommand( - stack_id, - (cwd) => DockerCompose.upAll({ cwd }), - "starting", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting" + ); } export async function pullStackImages(stack_id: number): Promise { - try { - await runStackCommand( - stack_id, - (cwd) => DockerCompose.pullAll({ cwd }), - "pulling images for", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images" + ); } export async function restartStack(stack_id: number): Promise { - try { - await runStackCommand( - stack_id, - (cwd) => DockerCompose.restartAll({ cwd }), - "restarting", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting" + ); } -export async function getStackStatus(stack_id: number): Promise { - try { - return await runStackCommand( - stack_id, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "retrieving status for", - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } +export async function getStackStatus( + stack_id: number +): Promise> { + // Wrap the returned status value to match Promise if that is the expectation. + // In this case, if you need the status, you might adjust the type signature. + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check" + ); + return status; } export async function removeStack(stack_id: number): Promise { try { await runStackCommand( stack_id, - async (cwd) => { - await DockerCompose.down({ cwd }); + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }); }, - "removing", + "removing" ); const stackName = await getStackName(stack_id); - - const stack = { + const stackPath = await getStackPath({ + id: stack_id, name: stackName, - }; - - const stackPath = await getStackPath(stack as Stack); + } as Stack); try { await rm(stackPath, { recursive: true }); } catch (error: any) { - if (error.code === "ENOENT") { - console.log("Directory doesn't exist"); - } else { - throw error; - } + if (error.code !== "ENOENT") throw error; } + dbFunctions.deleteStack(stack_id); - logger.info(`Stack ${stackName} (${stack_id}) removed successfully`); + postToClient({ + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); throw new Error(errorMsg); } } @@ -227,19 +311,17 @@ export async function getAllStacksStatus(): Promise> { return acc; }, {}); }, - "retrieving status for", + "status-check" ); - return { stackName: stack.name, status }; - }), + return { stackId: stack.id, status }; + }) ); - return statusResults.reduce( - (acc, { stackName, status }) => { - acc[stackName] = status; - return acc; - }, - {} as Record, - ); + return statusResults.reduce((acc, { stackId, status }) => { + // Ensure stackId is used as a string if necessary, e.g. + acc[String(stackId)] = status; + return acc; + }, {} as Record); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error(errorMsg); diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index 3f12f956..60ab40b5 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -30,7 +30,7 @@ const calculateCpuPercent = (stats: Docker.ContainerStats): number => { const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { if (stats == null) { - return 0.0; + return 0; } const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; diff --git a/src/core/utils/change-me-checker.ts b/src/core/utils/change-me-checker.ts index fa19520b..74860488 100644 --- a/src/core/utils/change-me-checker.ts +++ b/src/core/utils/change-me-checker.ts @@ -11,6 +11,8 @@ export async function checkFileForChangeMe(filePath: string) { } if (regex.test(content)) { - throw new Error(`Error: The file contains 'CHANGE_ME'. Please update it.`); + throw new Error( + `The file contains ${regex.exec(content)}. Please update it.` + ); } } diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts index 7f3a99ca..689e5667 100644 --- a/src/core/utils/helpers.ts +++ b/src/core/utils/helpers.ts @@ -3,13 +3,9 @@ import { logger } from "./logger"; export function findObjectByKey( array: T[], key: keyof T, - value: T[keyof T], + value: T[keyof T] ): T | undefined { + logger.debug(`Searching ${String(key)}`); const data = array.find((item) => item[key] === value); - logger.debug( - `Searching ${String(key)} = ${String(value)} in ${String( - JSON.stringify(array), - )} Found Item ${JSON.stringify(data)}`, - ); return data; } diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 6c0c5eea..a10dfd23 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -1,11 +1,14 @@ -import { createLogger, format, transports } from "winston"; -import type { TransformableInfo } from "logform"; import path from "path"; +import wrapAnsi from "wrap-ansi"; import chalk, { ChalkInstance } from "chalk"; +import type { TransformableInfo } from "logform"; +import { createLogger, format, transports } from "winston"; + import { dbFunctions } from "~/core/database"; -import wrapAnsi from "wrap-ansi"; + import { logToClients } from "~/routes/live-logs"; -import type { logStreamData } from "~/typings/websocket"; + +import { log_message } from "~/typings/database"; const padNewlines = process.env.PAD_NEW_LINES !== "false"; @@ -19,15 +22,6 @@ type LogLevel = | "task" | "ut"; -interface CustomTransformableInfo extends TransformableInfo { - file: string; - line: number; -} - -type LogStreamData = Omit & { - message: string; -}; - const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; const formatTerminalMessage = (message: string, prefix: string): string => { @@ -67,55 +61,30 @@ const levelColors: Record = { ut: chalk.hex("#9D00FF"), }; -const handleWebSocketLog = ( - level: string, - timestamp: string, - message: string, - file: string, - line: number, -) => { +const handleWebSocketLog = (log: log_message) => { try { - const data = { - timestamp, - level: level, - message: message, - file: file, - line: line, - }; - - logToClients(data); + logToClients(log); } catch (error) { console.error( - `WebSocket logging failed: ${error instanceof Error ? error.message : error}`, + `WebSocket logging failed: ${ + error instanceof Error ? error.message : error + }` ); } }; -const handleDatabaseLog = ( - level: string, - timestamp: string, - message: string, - file: string, - line: number, -): void => { +const handleDatabaseLog = (log: log_message): void => { try { - const data = { - timestamp, - level, - message, - file: file, - line: line, - }; - - dbFunctions.addLogEntry(data); + dbFunctions.addLogEntry(log); } catch (error) { console.error( - `Database logging failed: ${error instanceof Error ? error.message : error}`, + `Database logging failed: ${ + error instanceof Error ? error.message : error + }` ); } }; -// Main logger export const logger = createLogger({ level: process.env.LOG_LEVEL || "debug", format: format.combine( @@ -145,7 +114,7 @@ export const logger = createLogger({ })(), format.printf((info) => { const { timestamp, level, message, file, line } = - info as CustomTransformableInfo; + info as TransformableInfo & log_message; let processedLevel = level as LogLevel; let processedMessage = String(message); @@ -166,38 +135,40 @@ export const logger = createLogger({ } if (file.endsWith("plugin.ts")) { - processedMessage = `[ ${chalk.greenBright("Plugin")} ] ${processedMessage}`; + processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; } const paddedLevel = processedLevel.toUpperCase().padEnd(5); const coloredLevel = (levelColors[processedLevel] || chalk.white)( - paddedLevel, + paddedLevel ); const coloredContext = chalk.cyan(`${file}:${line}`); const coloredTimestamp = chalk.yellow(timestamp); const prefix = `${paddedLevel} [ ${timestamp} ] - `; + const combinedContent = `${processedMessage} - ${coloredContext}`; + const formattedMessage = padNewlines - ? formatTerminalMessage(processedMessage, prefix) - : processedMessage; - - handleDatabaseLog( - coloredTimestamp.replace(ansiRegex, "").trim(), - coloredLevel.replace(ansiRegex, "").trim(), - processedMessage.replace(ansiRegex, "").trim(), - file.trim(), + ? formatTerminalMessage(combinedContent, prefix) + : combinedContent; + + handleDatabaseLog({ + level: processedLevel, + timestamp, + message: processedMessage, + file, line, - ); - handleWebSocketLog( - coloredLevel.replace(ansiRegex, "").trim(), - coloredTimestamp.replace(ansiRegex, "").trim(), - processedMessage.replace(ansiRegex, "").trim(), - file.trim(), + }); + handleWebSocketLog({ + level: processedLevel, + timestamp, + message: processedMessage, + file, line, - ); + }); - return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage} - [ ${coloredContext} ]`; - }), + return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; + }) ), transports: [new transports.Console()], }); diff --git a/src/core/utils/package-json.ts b/src/core/utils/package-json.ts index 3872d561..9147d2f1 100644 --- a/src/core/utils/package-json.ts +++ b/src/core/utils/package-json.ts @@ -1,10 +1,17 @@ import packageJson from "~/../package.json"; -const { version, description, license, contributors, dependencies, devDependencies } = packageJson; + +const { version, description, license, dependencies, devDependencies } = + packageJson; +let { contributors } = packageJson; const authorName = packageJson.author.name; const authorEmail = packageJson.author.email; const authorWebsite = packageJson.author.url; +if ((contributors = [])) { + contributors = [":(" as never]; +} + export { version, description, diff --git a/src/core/utils/response-handler.ts b/src/core/utils/response-handler.ts index 369e9171..60a11ea0 100644 --- a/src/core/utils/response-handler.ts +++ b/src/core/utils/response-handler.ts @@ -1,4 +1,5 @@ import { logger } from "~/core/utils/logger"; + import type { set } from "~/typings/elysiajs"; export const responseHandler = { @@ -6,7 +7,7 @@ export const responseHandler = { set: set, error: string, response_message: string, - error_code?: number, + error_code?: number ) { set.status = error_code || 500; logger.error(`${response_message} - ${error}`); diff --git a/src/index.ts b/src/index.ts index 16cd5eeb..cf5edef3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,23 +1,33 @@ -import { dbFunctions } from "~/core/database"; -import { swagger } from "@elysiajs/swagger"; import { Elysia } from "elysia"; -import { loadPlugins } from "~/core/plugins/loader"; +import staticPlugin from "@elysiajs/static"; +import { swagger } from "@elysiajs/swagger"; +import { serverTiming } from "@elysiajs/server-timing"; + import { logger } from "~/core/utils/logger"; +import { dbFunctions } from "~/core/database"; +import { loadPlugins } from "~/core/plugins/loader"; +import { setSchedules } from "~/core/docker/scheduler"; +import { monitorDockerEvents } from "~/core/docker/monitor"; +import { swaggerReadme } from "~/core/utils/swagger-readme"; +import { + authorWebsite, + contributors, + license, +} from "~/core/utils/package-json"; + +import { validateApiKey } from "~/middleware/auth"; + +import { backendLogs } from "~/routes/logs"; +import { utilRoutes } from "~/routes/utils"; +import { liveLogs } from "~/routes/live-logs"; +import { stackRoutes } from "~/routes/stacks"; +import { apiConfigRoutes } from "~/routes/api-config"; import { dockerRoutes } from "~/routes/docker-manager"; import { dockerStatsRoutes } from "~/routes/docker-stats"; -import { backendLogs } from "~/routes/logs"; import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; -import { stackRoutes } from "./routes/stacks"; -import { apiConfigRoutes } from "~/routes/api-config"; -import { setSchedules } from "~/core/docker/scheduler"; -import { serverTiming } from "@elysiajs/server-timing"; -import staticPlugin from "@elysiajs/static"; -import { config } from "./typings/database"; -import { validateApiKey } from "./middleware/auth"; -import { monitorDockerEvents } from "./core/docker/monitor"; -import { liveLogs } from "./routes/live-logs"; -import { utilRoutes } from "./routes/utils"; -import { swaggerReadme } from "./core/utils/swagger-readme"; +import { liveStacks } from "./routes/live-stacks"; + +import { config } from "~/typings/database"; console.log(""); @@ -69,7 +79,7 @@ export const DockStatAPI = new Elysia() }, ], }, - }), + }) ) .onBeforeHandle(async (context) => { const { path, request, set } = context; @@ -96,6 +106,7 @@ export const DockStatAPI = new Elysia() .use(stackRoutes) .use(utilRoutes) .use(liveLogs) + .use(liveStacks) .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) .onError(({ code, set, path }) => { if (code === "NOT_FOUND") { @@ -129,7 +140,7 @@ async function startServer() { if (apiKey === "changeme") { logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" ); } @@ -138,11 +149,11 @@ async function startServer() { console.log("----- [ ############## ]"); logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, - ); - logger.info( - `tRPC Endpoint available at: http://${hostname}:${port}/trpc`, + `Swagger API Documentation available at http://${hostname}:${port}/swagger` ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); }); } catch (error) { logger.error("Failed to start server:", error); diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 17ae2c6b..48aad394 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -1,7 +1,8 @@ -import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; -import { config } from "~/typings/database"; +import { dbFunctions } from "~/core/database"; + import { set } from "~/typings/elysiajs"; +import { config } from "~/typings/database"; export async function hashApiKey(apiKey: string): Promise { logger.debug("Hashing API key"); @@ -16,7 +17,7 @@ export async function hashApiKey(apiKey: string): Promise { async function validateApiKeyHash( providedKey: string, - storedHash: string, + storedHash: string ): Promise { logger.debug("Validating API key hash"); try { @@ -30,7 +31,7 @@ async function validateApiKeyHash( } async function getApiKeyFromDb( - apiKey: string, + apiKey: string ): Promise<{ hash: string } | null> { const dbApiKey = (dbFunctions.getConfig() as config[])[0].api_key; logger.debug(`Querying database for API key: ${apiKey}`); @@ -44,7 +45,7 @@ export async function validateApiKey(request: Request, set: set) { if (process.env.NODE_ENV != "production") { logger.warn( - "API Key validation deactivated, since running in development mode", + "API Key validation deactivated, since running in development mode" ); return { apiKey }; } else if (!apiKey) { diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index d1a1ec61..e9a97750 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -1,6 +1,7 @@ +import { logger } from "~/core/utils/logger"; + import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo } from "~/typings/docker"; -import { logger } from "~/core/utils/logger"; // See https://outline.itsnik.de/s/dockstat/doc/plugin-development-3UBj9gNMKF for more info @@ -9,67 +10,67 @@ const ExamplePlugin: Plugin = { async onContainerStart(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} started on ${containerInfo.hostId}`, + `Container ${containerInfo.name} started on ${containerInfo.hostId}` ); }, async onContainerStop(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} stopped on ${containerInfo.hostId}`, + `Container ${containerInfo.name} stopped on ${containerInfo.hostId}` ); }, async onContainerExit(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} exited on ${containerInfo.hostId}`, + `Container ${containerInfo.name} exited on ${containerInfo.hostId}` ); }, async onContainerCreate(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} created on ${containerInfo.hostId}`, + `Container ${containerInfo.name} created on ${containerInfo.hostId}` ); }, async onContainerDestroy(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}`, + `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}` ); }, async onContainerPause(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} pause on ${containerInfo.hostId}`, + `Container ${containerInfo.name} pause on ${containerInfo.hostId}` ); }, async onContainerUnpause(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} resumed on ${containerInfo.hostId}`, + `Container ${containerInfo.name} resumed on ${containerInfo.hostId}` ); }, async onContainerRestart(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} restarted on ${containerInfo.hostId}`, + `Container ${containerInfo.name} restarted on ${containerInfo.hostId}` ); }, async onContainerUpdate(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} updated on ${containerInfo.hostId}`, + `Container ${containerInfo.name} updated on ${containerInfo.hostId}` ); }, async onContainerRename(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} renamed on ${containerInfo.hostId}`, + `Container ${containerInfo.name} renamed on ${containerInfo.hostId}` ); }, async onContainerHealthStatus(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} changed status to ${containerInfo.status}`, + `Container ${containerInfo.name} changed status to ${containerInfo.status}` ); }, @@ -83,13 +84,13 @@ const ExamplePlugin: Plugin = { async handleContainerDie(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} died on ${containerInfo.hostId}`, + `Container ${containerInfo.name} died on ${containerInfo.hostId}` ); }, async onContainerKill(containerInfo: ContainerInfo) { logger.info( - `Container ${containerInfo.name} killed on ${containerInfo.hostId}`, + `Container ${containerInfo.name} killed on ${containerInfo.hostId}` ); }, } satisfies Plugin; diff --git a/src/plugins/telegram.plugin.ts b/src/plugins/telegram.plugin.ts index cf7c376d..eaec24e0 100644 --- a/src/plugins/telegram.plugin.ts +++ b/src/plugins/telegram.plugin.ts @@ -1,6 +1,7 @@ +import { logger } from "~/core/utils/logger"; + import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo } from "~/typings/docker"; -import { logger } from "~/core/utils/logger"; const TELEGRAM_BOT_TOKEN = "CHANGE_ME"; // Replace with your bot token const TELEGRAM_CHAT_ID = "CHANGE_ME"; // Replace with your chat ID @@ -19,7 +20,7 @@ const TelegramNotificationPlugin: Plugin = { chat_id: TELEGRAM_CHAT_ID, text: message, }), - }, + } ); if (!response.ok) { logger.error(`HTTP error ${response.status}`); diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 9861d3f9..0b2c4fb2 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -1,8 +1,9 @@ import { Elysia, t } from "elysia"; -import { dbFunctions } from "~/core/database"; + import { logger } from "~/core/utils/logger"; +import { dbFunctions } from "~/core/database"; +import { pluginManager } from "~/core/plugins/plugin-manager"; import { responseHandler } from "~/core/utils/response-handler"; -import { config } from "~/typings/database"; import { version, authorEmail, @@ -14,8 +15,10 @@ import { devDependencies, license, } from "~/core/utils/package-json"; + import { hashApiKey } from "~/middleware/auth"; -import { pluginManager } from "~/core/plugins/plugin-manager"; + +import { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) .get( @@ -32,7 +35,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error getting the DockStatAPI config", + "Error getting the DockStatAPI config" ); } }, @@ -42,7 +45,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) description: "Returns current API configuration including data retention policies and security settings", }, - }, + } ) .get( "/plugins", @@ -53,7 +56,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error getting all registered plugins", + "Error getting all registered plugins" ); } }, @@ -63,7 +66,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) description: "Lists all active plugins with their registration details and status", }, - }, + } ) .post( "/update", @@ -74,14 +77,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) dbFunctions.updateConfig( fetching_interval, keep_data_for, - await hashApiKey(api_key), + await hashApiKey(api_key) ); return responseHandler.ok(set, "Updated DockStatAPI config"); } catch (error) { return responseHandler.error( set, "Error updating the DockStatAPI config", - error as string, + error as string ); } }, @@ -96,7 +99,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) description: "Modifies core API settings including data collection intervals, retention periods, and security credentials", }, - }, + } ) .get( "/package", @@ -118,7 +121,7 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.error( set, error as string, - "Error while reading package.json", + "Error while reading package.json" ); } }, @@ -128,5 +131,5 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) description: "Displays package metadata including dependencies, contributors, and licensing information", }, - }, + } ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 635b78bb..35747518 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -1,7 +1,9 @@ import { Elysia, t } from "elysia"; -import { dbFunctions } from "~/core/database"; + import { logger } from "~/core/utils/logger"; +import { dbFunctions } from "~/core/database"; import { responseHandler } from "~/core/utils/response-handler"; + import { DockerHost } from "~/typings/docker"; export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) @@ -16,7 +18,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) return responseHandler.error( set, "Error adding docker Host", - error as string, + error as string ); } }, @@ -31,7 +33,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) hostAddress: t.String(), secure: t.Boolean(), }), - }, + } ) .post( @@ -45,7 +47,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) return responseHandler.error( set, error as string, - "Failed to update host", + "Failed to update host" ); } }, @@ -61,7 +63,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) hostAddress: t.String(), secure: t.Boolean(), }), - }, + } ) .get( @@ -76,7 +78,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) return responseHandler.error( set, error as string, - "Failed to retrieve hosts", + "Failed to retrieve hosts" ); } }, @@ -86,7 +88,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) description: "Lists all configured Docker hosts with their connection settings", }, - }, + } ) .delete( @@ -100,7 +102,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) return responseHandler.error( set, error as string, - "Failed to delete host", + "Failed to delete host" ); } }, @@ -113,5 +115,5 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) params: t.Object({ id: t.Number(), }), - }, + } ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 9cfe465b..3c31c5c9 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -1,16 +1,18 @@ import Docker from "dockerode"; import { Elysia } from "elysia"; + +import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; +import { findObjectByKey } from "~/core/utils/helpers"; +import { responseHandler } from "~/core/utils/response-handler"; import { calculateCpuPercent, calculateMemoryUsage, } from "~/core/utils/calculations"; -import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/response-handler"; -import { findObjectByKey } from "~/core/utils/helpers"; -import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; + import type { DockerInfo } from "~/typings/dockerode"; +import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) .get( diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 8b26542b..3165eff9 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -1,15 +1,16 @@ +import split2 from "split2"; import { Elysia } from "elysia"; +import type { Readable } from "stream"; import type { ElysiaWS } from "elysia/dist/ws"; + +import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; +import { responseHandler } from "~/core/utils/response-handler"; import { calculateCpuPercent, calculateMemoryUsage, } from "~/core/utils/calculations"; -import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/response-handler"; -import split2 from "split2"; -import type { Readable } from "stream"; const activeDockerConnections = new Set>(); const connectionStreams = new Map< diff --git a/src/routes/live-logs.ts b/src/routes/live-logs.ts index 1232ce5a..17c4c69e 100644 --- a/src/routes/live-logs.ts +++ b/src/routes/live-logs.ts @@ -1,7 +1,9 @@ import { Elysia } from "elysia"; import type { ElysiaWS } from "elysia/dist/ws"; + import { logger } from "~/core/utils/logger"; -import type { logStreamData } from "~/typings/websocket"; + +import { log_message } from "~/typings/database"; const activeConnections = new Set>(); @@ -17,7 +19,7 @@ export const liveLogs = new Elysia({ prefix: "/logs" }).ws("/ws", { }, }); -export function logToClients(data: logStreamData) { +export function logToClients(data: log_message) { activeConnections.forEach((ws) => { try { ws.send(JSON.stringify(data)); diff --git a/src/routes/live-stacks.ts b/src/routes/live-stacks.ts new file mode 100644 index 00000000..4f3d395c --- /dev/null +++ b/src/routes/live-stacks.ts @@ -0,0 +1,30 @@ +import { Elysia } from "elysia"; +import type { ElysiaWS } from "elysia/dist/ws"; + +import { logger } from "~/core/utils/logger"; +import { stackSocketMessage } from "~/typings/websocket"; + +const activeConnections = new Set>(); + +export const liveStacks = new Elysia().ws("/stacks", { + open(ws) { + activeConnections.add(ws); + ws.send({ message: "Connection established" }); + logger.info(`New Stacks WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Stacks WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, +}); + +export function postToClient(data: stackSocketMessage) { + activeConnections.forEach((ws) => { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + }); +} diff --git a/src/routes/logs.ts b/src/routes/logs.ts index dc7fc374..ce33235a 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -1,6 +1,7 @@ import { Elysia } from "elysia"; -import { dbFunctions } from "~/core/database"; + import { logger } from "~/core/utils/logger"; +import { dbFunctions } from "~/core/database"; export const backendLogs = new Elysia({ prefix: "/logs" }) .get( @@ -23,7 +24,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) description: "Retrieves complete application log history from persistent storage", }, - }, + } ) .get( @@ -46,7 +47,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) description: "Filters logs by severity level (debug, info, warn, error, fatal)", }, - }, + } ) .delete( @@ -68,7 +69,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) tags: ["Management"], description: "Purges all historical log records from the database", }, - }, + } ) .delete( @@ -90,5 +91,5 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) tags: ["Management"], description: "Clears log entries matching specified severity level", }, - }, + } ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index eea41609..528a5c1a 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -1,4 +1,7 @@ import { Elysia, t } from "elysia"; + +import { logger } from "~/core/utils/logger"; +import { dbFunctions } from "~/core/database"; import { responseHandler } from "~/core/utils/response-handler"; import { deployStack, @@ -10,8 +13,6 @@ import { getAllStacksStatus, removeStack, } from "~/core/stacks/controller"; -import { dbFunctions } from "~/core/database"; -import { logger } from "~/core/utils/logger"; export const stackRoutes = new Elysia({ prefix: "/stacks" }) .post( @@ -49,18 +50,18 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body.automatic_reboot_on_error, isCustom, image_updates, - body.stack_prefix, + body.stack_prefix ); logger.info(`Deployed Stack (${body.name})`); return responseHandler.ok( set, - `Stack ${body.name} deployed successfully`, + `Stack ${body.name} deployed successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error deploying stack", + "Error deploying stack" ); } }, @@ -80,7 +81,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) source: t.String(), stack_prefix: t.Optional(t.String()), }), - }, + } ) .post( "/start", @@ -93,13 +94,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Started Stack (${body.stackId})`); return responseHandler.ok( set, - `Stack ${body.stackId} started successfully`, + `Stack ${body.stackId} started successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error starting stack", + "Error starting stack" ); } }, @@ -112,7 +113,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stackId: t.Number(), }), - }, + } ) .post( "/stop", @@ -125,13 +126,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Stopped Stack (${body.stackId})`); return responseHandler.ok( set, - `Stack ${body.stackId} stopped successfully`, + `Stack ${body.stackId} stopped successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error stopping stack", + "Error stopping stack" ); } }, @@ -144,7 +145,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stackId: t.Number(), }), - }, + } ) .post( "/restart", @@ -157,13 +158,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Restarted Stack (${body.stackId})`); return responseHandler.ok( set, - `Stack ${body.stackId} restarted successfully`, + `Stack ${body.stackId} restarted successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error restarting stack", + "Error restarting stack" ); } }, @@ -176,7 +177,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stackId: t.Number(), }), - }, + } ) .post( "/pull-images", @@ -189,13 +190,13 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) logger.info(`Pulled Stack images (${body.stackId})`); return responseHandler.ok( set, - `Images for stack ${body.stackId} pulled successfully`, + `Images for stack ${body.stackId} pulled successfully` ); } catch (error: any) { return responseHandler.error( set, error.message || error, - "Error pulling images", + "Error pulling images" ); } }, @@ -208,7 +209,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stackId: t.Number(), }), - }, + } ) .get( "/status", @@ -220,7 +221,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) status = await getStackStatus(query.stackId); res = responseHandler.ok( set, - `Stack ${query.stackId} status retrieved successfully`, + `Stack ${query.stackId} status retrieved successfully` ); logger.info("Fetched Stack status"); } else { @@ -233,7 +234,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stack status", + "Error getting stack status" ); } }, @@ -246,7 +247,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) query: t.Object({ stackId: t.Number(), }), - }, + } ) .get( "/", @@ -259,7 +260,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error getting stacks", + "Error getting stacks" ); } }, @@ -269,7 +270,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) description: "Lists all registered stacks with their complete configuration details", }, - }, + } ) .delete( @@ -284,7 +285,7 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) return responseHandler.error( set, error.message || error, - "Error deleting stack", + "Error deleting stack" ); } }, @@ -297,5 +298,5 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) body: t.Object({ stackId: t.Number(), }), - }, + } ); diff --git a/src/routes/utils.ts b/src/routes/utils.ts index b61e0e7d..17cba245 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -1,4 +1,6 @@ import { Elysia, t } from "elysia"; + +import { responseHandler } from "~/core/utils/response-handler"; import { version, authorEmail, @@ -10,7 +12,6 @@ import { devDependencies, license, } from "~/core/utils/package-json"; -import { responseHandler } from "~/core/utils/response-handler"; export const utilRoutes = new Elysia({ prefix: "/utils" }).get( "/info", @@ -32,7 +33,7 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( return responseHandler.error( set, error.message || error, - "Error getting DockStatAPI information", + "Error getting DockStatAPI information" ); } }, @@ -42,5 +43,5 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( description: "Retrieves DockStatAPI metadata including version, author information, dependencies, and licensing details", }, - }, + } ); diff --git a/src/tests/cleanup.ts b/src/tests/cleanup.ts index 20b965b9..31756dda 100644 --- a/src/tests/cleanup.ts +++ b/src/tests/cleanup.ts @@ -1,7 +1,8 @@ import { dbFunctions } from "~/core/database"; -import type { DockerHost } from "~/typings/docker"; import { findObjectByKey } from "~/core/utils/helpers"; +import type { DockerHost } from "~/typings/docker"; + console.log(""); console.log("Deleting `test` Docker host"); diff --git a/src/tests/delete.spec.ts b/src/tests/delete.spec.ts index 37e36fc9..097ede0d 100644 --- a/src/tests/delete.spec.ts +++ b/src/tests/delete.spec.ts @@ -1,4 +1,5 @@ import { describe, it } from "bun:test"; + import { runTestCode } from "./helper"; describe("DockStatAPI (DELETE)", () => { diff --git a/src/tests/gets.spec.ts b/src/tests/gets.spec.ts index 27235083..7b103151 100644 --- a/src/tests/gets.spec.ts +++ b/src/tests/gets.spec.ts @@ -1,5 +1,5 @@ import { describe, it } from "bun:test"; -import { runTestResponse, runTestCode } from "./helper"; + import { version, authorEmail, @@ -12,6 +12,8 @@ import { license, } from "~/core/utils/package-json"; +import { runTestResponse, runTestCode } from "./helper"; + describe("DockStatAPI (GET)", () => { it("Check Server connection", async () => { await runTestResponse("/health", '{"status":"healthy"}', "GET"); diff --git a/src/tests/helper.ts b/src/tests/helper.ts index bd03055f..59ec0394 100644 --- a/src/tests/helper.ts +++ b/src/tests/helper.ts @@ -1,7 +1,9 @@ import { expect } from "bun:test"; -import { DockStatAPI } from ".."; + import { logger } from "~/core/utils/logger"; +import { DockStatAPI } from ".."; + export const API_KEY = "TestKey"; const server = "http://localhost:3001"; diff --git a/src/tests/post.spec.ts b/src/tests/post.spec.ts index 9ce64e9f..9747e67f 100644 --- a/src/tests/post.spec.ts +++ b/src/tests/post.spec.ts @@ -1,5 +1,7 @@ import { describe, it } from "bun:test"; + import { runTestResponse, runTestCode } from "./helper"; + import { DockerHost } from "~/typings/docker"; describe("DockStatAPI (POST)", () => { @@ -36,7 +38,7 @@ describe("DockStatAPI (POST)", () => { await runTestResponse( "/docker-config/hosts", JSON.stringify(responseBody), - "GET", + "GET" ); }); diff --git a/src/typings/database.ts b/src/typings/database.ts index 96319e96..67d8121a 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -16,4 +16,12 @@ interface stacks_config { image_updates: boolean; } -export type { config, stacks_config }; +interface log_message { + level: string; + timestamp: string; + message: string; + file: string; + line: number; +} + +export type { config, stacks_config, log_message }; diff --git a/src/typings/plugin.ts b/src/typings/plugin.ts index ee16559c..6ca68bf8 100644 --- a/src/typings/plugin.ts +++ b/src/typings/plugin.ts @@ -1,5 +1,4 @@ import { ContainerInfo } from "~/typings/docker"; -import { HostStats } from "~/typings/docker"; interface Plugin { name: string; diff --git a/src/typings/websocket.ts b/src/typings/websocket.ts index 1cb3821f..5635e3c8 100644 --- a/src/typings/websocket.ts +++ b/src/typings/websocket.ts @@ -1,17 +1,15 @@ -//import type { Readable, Transform } from "stream"; -//import type internal from "stream"; - -//interface streams { -// statsStream: Readable; -// splitStream: internal.Transform; -//} +interface stackSocketMessage { + message?: string; + type?: "stack-progress" | "stack-error" | "stack-status" | "stack-removed"; + data?: stackSocketData; +} -interface logStreamData { - timestamp: string; - level: string; +interface stackSocketData { + stack_id: number; message: string; - file: string; - line: number; + action?: string; + status?: string; + timestamp?: string; } -export { logStreamData }; +export { stackSocketMessage }; diff --git a/tsconfig.json b/tsconfig.json index ab566ad1..9c2d5112 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,7 +23,7 @@ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - + "outDir": "build/", /* Modules */ "module": "ES2022" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ From d62c682949e9930f16c03743c98ef89dc1d39bb8 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 15 Apr 2025 17:45:35 +0000 Subject: [PATCH 238/369] Update dependency graphs --- dependency-graph.mmd | 357 +++++------ dependency-graph.svg | 1349 ++++++++++++++++++++++-------------------- 2 files changed, 881 insertions(+), 825 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 844ace0b..1ba62544 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -8,203 +8,210 @@ flowchart LR subgraph 0["src"] 1["index.ts"] -subgraph 2["core"] -subgraph 3["docker"] -4["monitor.ts"] -V["client.ts"] -1A["scheduler.ts"] -1B["store-host-stats.ts"] -1D["store-container-stats.ts"] +subgraph 2["routes"] +3["live-stacks.ts"] +P["live-logs.ts"] +1D["api-config.ts"] +1F["docker-manager.ts"] +1G["docker-stats.ts"] +1H["docker-websocket.ts"] +1J["logs.ts"] +1K["stacks.ts"] +1N["utils.ts"] end -subgraph 6["plugins"] -7["plugin-manager.ts"] -1F["loader.ts"] +subgraph 4["core"] +subgraph 5["utils"] +6["logger.ts"] +M["helpers.ts"] +10["calculations.ts"] +15["change-me-checker.ts"] +17["package-json.ts"] +19["swagger-readme.ts"] +1E["response-handler.ts"] end -subgraph 9["utils"] -A["logger.ts"] -W["swagger-readme.ts"] -15["helpers.ts"] -16["response-handler.ts"] -18["package-json.ts"] -1E["calculations.ts"] -1G["change-me-checker.ts"] +subgraph 8["database"] +9["index.ts"] +A["config.ts"] +B["database.ts"] +D["helper.ts"] +E["containerStats.ts"] +F["dockerHosts.ts"] +I["hostStats.ts"] +J["logs.ts"] +L["stacks.ts"] end -subgraph C["database"] -D["index.ts"] -E["config.ts"] -F["database.ts"] -H["helper.ts"] -I["containerStats.ts"] -J["dockerHosts.ts"] -M["hostStats.ts"] -N["logs.ts"] -P["stacks.ts"] +subgraph Q["docker"] +R["monitor.ts"] +X["client.ts"] +Y["scheduler.ts"] +Z["store-container-stats.ts"] +11["store-host-stats.ts"] end -subgraph 11["stacks"] -12["controller.ts"] +subgraph T["plugins"] +U["plugin-manager.ts"] +13["loader.ts"] end +subgraph 1L["stacks"] +1M["controller.ts"] end -subgraph K["typings"] -L["docker.ts"] -O["websocket.ts"] -Q["database.ts"] -R["docker-compose.ts"] -U["plugin.ts"] -Z["elysiajs.ts"] -1C["dockerode.ts"] end -subgraph S["routes"] -T["live-logs.ts"] -10["stacks.ts"] -17["utils.ts"] -1H["api-config.ts"] -1I["docker-manager.ts"] -1J["docker-stats.ts"] -1K["docker-websocket.ts"] -1M["logs.ts"] +subgraph G["typings"] +H["docker.ts"] +K["websocket.ts"] +N["database.ts"] +O["docker-compose.ts"] +W["plugin.ts"] +12["dockerode.ts"] +1C["elysiajs.ts"] end -subgraph X["middleware"] -Y["auth.ts"] +subgraph 1A["middleware"] +1B["auth.ts"] end end -5["bun"] -8["events"] -B["path"] -G["bun:sqlite"] -subgraph 13["fs"] -14["promises"] +7["path"] +C["bun:sqlite"] +S["bun"] +V["events"] +subgraph 14["fs"] +16["promises"] end -19["package.json"] -1L["stream"] -1-->4 -1-->W +18["package.json"] +1I["stream"] +1-->3 +1-->9 +1-->R 1-->Y -1-->T -1-->10 +1-->13 +1-->6 1-->17 -1-->Q -1-->D -1-->1A +1-->19 +1-->1B +1-->1D 1-->1F -1-->A +1-->1G 1-->1H -1-->1I +1-->P 1-->1J 1-->1K -1-->1M -4-->7 -4-->D -4-->V -4-->A -4-->L -4-->L -4-->5 -7-->A -7-->L -7-->U -7-->8 -A-->D -A-->T -A-->O +1-->1N +1-->N +3-->6 +3-->K +6-->9 +6-->P +6-->N +6-->7 +9-->A +9-->E +9-->B +9-->F +9-->I +9-->J +9-->L A-->B -D-->E -D-->I -D-->F -D-->J -D-->M -D-->N -D-->P -E-->F -E-->H -F-->G -H-->A -I-->F +A-->D +B-->C +D-->6 +E-->B +E-->D +F-->B +F-->D +F-->H +I-->B +I-->D I-->H -J-->F -J-->H -J-->L -M-->F -M-->H -M-->L -N-->F -N-->H -N-->O -P-->F -P-->H -P-->Q -P-->R -T-->A -T-->O -U-->L -V-->A -V-->L -Y-->D -Y-->A -Y-->Q +J-->B +J-->D +J-->K +L-->M +L-->B +L-->D +L-->N +L-->O +M-->6 +P-->6 +P-->N +R-->U +R-->9 +R-->X +R-->6 +R-->H +R-->H +R-->S +U-->6 +U-->H +U-->W +U-->V +W-->H +X-->6 +X-->H +Y-->9 Y-->Z -10-->D -10-->12 -10-->A -10-->16 -12-->15 -12-->D -12-->A -12-->Q -12-->R -12-->14 -15-->A -16-->A -16-->Z +Y-->11 +Y-->6 +Y-->N +Z-->6 +Z-->9 +Z-->X +Z-->10 +11-->9 +11-->X +11-->M +11-->6 +11-->H +11-->12 +13-->15 +13-->6 +13-->U +13-->14 +13-->7 +15-->6 +15-->16 17-->18 -17-->16 -18-->19 -1A-->D -1A-->1B -1A-->1D -1A-->A -1A-->Q -1B-->D -1B-->V -1B-->15 -1B-->A -1B-->L +1B-->9 +1B-->6 +1B-->N 1B-->1C -1D-->A -1D-->D -1D-->V +1D-->9 +1D-->U +1D-->6 +1D-->17 1D-->1E -1F-->1G -1F-->A -1F-->7 -1F-->13 -1F-->B -1G-->A -1G-->14 -1H-->D -1H-->7 -1H-->A -1H-->18 -1H-->16 -1H-->Y -1H-->Q -1I-->D -1I-->A -1I-->16 -1I-->L -1J-->D -1J-->V -1J-->1E -1J-->15 -1J-->A -1J-->16 -1J-->L -1J-->1C -1K-->D -1K-->V +1D-->1B +1D-->N +1E-->6 +1E-->1C +1F-->9 +1F-->6 +1F-->1E +1F-->H +1G-->9 +1G-->X +1G-->10 +1G-->M +1G-->6 +1G-->1E +1G-->H +1G-->12 +1H-->9 +1H-->X +1H-->10 +1H-->6 +1H-->1E +1H-->1I +1J-->9 +1J-->6 +1K-->9 +1K-->1M +1K-->6 1K-->1E -1K-->A -1K-->16 -1K-->1L -1M-->D -1M-->A +1M-->M +1M-->9 +1M-->6 +1M-->3 +1M-->N +1M-->O +1M-->16 +1N-->17 +1N-->1E diff --git a/dependency-graph.svg b/dependency-graph.svg index 0549c66b..34974a2d 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,72 +4,72 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -77,8 +77,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -86,8 +86,8 @@ events - -events + +events @@ -95,8 +95,8 @@ fs - -fs + +fs @@ -104,8 +104,8 @@ fs/promises - -promises + +promises @@ -113,8 +113,8 @@ package.json - -package.json + +package.json @@ -122,8 +122,8 @@ path - -path + +path @@ -131,8 +131,8 @@ src/core/database/config.ts - -config.ts + +config.ts @@ -140,1182 +140,1231 @@ src/core/database/database.ts - -database.ts + +database.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/helper.ts - -helper.ts + +helper.ts src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/database.ts->bun:sqlite - - + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + src/typings/docker.ts - -docker.ts + +docker.ts src/core/database/dockerHosts.ts->src/typings/docker.ts - - + + - + src/core/utils/logger.ts->path - - + + src/core/database/index.ts - -index.ts + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + - - -src/typings/websocket.ts - - -websocket.ts + + +src/typings/database.ts + + +database.ts - - -src/core/utils/logger.ts->src/typings/websocket.ts - - + + +src/core/utils/logger.ts->src/typings/database.ts + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts->src/typings/docker.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + + + + +src/typings/websocket.ts + + +websocket.ts + + src/core/database/logs.ts->src/typings/websocket.ts - - + + - + src/core/database/stacks.ts->src/core/database/database.ts - - + + - + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + - + -src/typings/database.ts - - -database.ts +src/core/utils/helpers.ts + + +helpers.ts + + +src/core/database/stacks.ts->src/core/utils/helpers.ts + + + + + - + src/core/database/stacks.ts->src/typings/database.ts - - + + - + src/typings/docker-compose.ts - - -docker-compose.ts + + +docker-compose.ts - + src/core/database/stacks.ts->src/typings/docker-compose.ts - - + + + + + +src/core/utils/helpers.ts->src/core/utils/logger.ts + + + + - + src/core/docker/client.ts - - -client.ts + + +client.ts - + src/core/docker/client.ts->src/typings/docker.ts - - + + - + src/core/docker/client.ts->src/core/utils/logger.ts - - + + - + src/core/docker/monitor.ts - - -monitor.ts + + +monitor.ts - + src/core/docker/monitor.ts->bun - - + + - + src/core/docker/monitor.ts->src/typings/docker.ts - - + + - + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + - + src/core/docker/monitor.ts->src/core/database/index.ts - - + + - + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + - + src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +plugin-manager.ts - + src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - + + - + src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/typings/plugin.ts - -plugin.ts + +plugin.ts - + src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + - + src/core/docker/scheduler.ts->src/typings/database.ts - - + + + + + +src/core/docker/store-container-stats.ts + + +store-container-stats.ts + + + + + +src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - - -src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +src/core/docker/store-container-stats.ts->src/core/utils/logger.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/database/index.ts + + + + + +src/core/docker/store-container-stats.ts->src/core/docker/client.ts + + + + + +src/core/utils/calculations.ts + + +calculations.ts - - -src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + +src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts + + - + src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - -src/core/docker/store-host-stats.ts->src/core/database/index.ts - - - - -src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - - - - -src/core/utils/helpers.ts - - -helpers.ts - - +src/core/docker/store-host-stats.ts->src/core/database/index.ts + + - + src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts - - + + + + + +src/core/docker/store-host-stats.ts->src/core/docker/client.ts + + src/typings/dockerode.ts - -dockerode.ts + +dockerode.ts - + src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - - - - -src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - - - - -src/core/docker/store-container-stats.ts->src/core/database/index.ts - - - - - -src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - - - - -src/core/utils/calculations.ts - - -calculations.ts - - - - - -src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - - - - -src/core/utils/helpers.ts->src/core/utils/logger.ts - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - + + + + + +src/core/stacks/controller.ts->src/core/utils/helpers.ts + + - + src/core/stacks/controller.ts->src/typings/database.ts - - + + - + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + - - -src/core/stacks/controller.ts->src/core/utils/helpers.ts - - + + +src/routes/live-stacks.ts + + +live-stacks.ts + + + + + +src/core/stacks/controller.ts->src/routes/live-stacks.ts + + + + + +src/routes/live-stacks.ts->src/core/utils/logger.ts + + + + + +src/routes/live-stacks.ts->src/typings/websocket.ts + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - - -src/routes/live-logs.ts->src/typings/websocket.ts - - + + +src/routes/live-logs.ts->src/typings/database.ts + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + src/typings/elysiajs.ts - - -elysiajs.ts + + +elysiajs.ts - + src/core/utils/response-handler.ts->src/typings/elysiajs.ts - - + + - + src/core/utils/swagger-readme.ts - - -swagger-readme.ts + + +swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->src/typings/database.ts - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + + + + +src/index.ts->src/routes/live-stacks.ts + + - + src/index.ts->src/routes/live-logs.ts - - + + + + + +src/index.ts->src/core/utils/package-json.ts + + - + src/index.ts->src/core/utils/swagger-readme.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - - - - -src/routes/stacks.ts - - -stacks.ts - - - - - -src/index.ts->src/routes/stacks.ts - - - - - -src/routes/utils.ts - - -utils.ts - - - - - -src/index.ts->src/routes/utils.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + + +src/routes/stacks.ts + + +stacks.ts + + + + +src/index.ts->src/routes/stacks.ts + + + + + +src/routes/utils.ts + + +utils.ts + + + + + +src/index.ts->src/routes/utils.ts + + + + + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - - - - -src/routes/stacks.ts->src/core/utils/logger.ts - - - - - -src/routes/stacks.ts->src/core/database/index.ts - - - - - -src/routes/stacks.ts->src/core/stacks/controller.ts - - - - - -src/routes/stacks.ts->src/core/utils/response-handler.ts - - - - - -src/routes/utils.ts->src/core/utils/package-json.ts - - - - - -src/routes/utils.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/typings/docker.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + + + + +src/routes/docker-stats.ts->src/core/utils/helpers.ts + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - - - - -src/routes/docker-stats.ts->src/core/utils/helpers.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + + + + +src/routes/stacks.ts->src/core/utils/logger.ts + + + + + +src/routes/stacks.ts->src/core/database/index.ts + + + + + +src/routes/stacks.ts->src/core/stacks/controller.ts + + + + + +src/routes/stacks.ts->src/core/utils/response-handler.ts + + + + + +src/routes/utils.ts->src/core/utils/package-json.ts + + + + + +src/routes/utils.ts->src/core/utils/response-handler.ts + + From cbb42fa46483a519225263ff45e4316f187ede37 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 08:17:13 +0200 Subject: [PATCH 239/369] Fix: Formatting --- src/core/stacks/controller.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 19bd0824..221bab4f 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -175,7 +175,6 @@ export async function deployStack( } export async function stopStack(stack_id: number): Promise { - // Note the await to discard the result (convert to void) await runStackCommand( stack_id, (cwd, progressCallback) => @@ -230,8 +229,6 @@ export async function restartStack(stack_id: number): Promise { export async function getStackStatus( stack_id: number ): Promise> { - // Wrap the returned status value to match Promise if that is the expectation. - // In this case, if you need the status, you might adjust the type signature. const status = await runStackCommand( stack_id, async (cwd) => { From a3b0699cd1386824b6fe2be50ede0cf8b5231d62 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:14:42 +0200 Subject: [PATCH 240/369] Feat: Linter, formatting and a bunch more --- .github/workflows/pipeline.yaml | 12 + .gitignore | 3 +- .knip.json | 6 +- biome.json | 26 + bun.lock | 21 + package.json | 98 +-- src/core/database/_dbState.ts | 5 + src/core/database/backup.ts | 163 +++++ src/core/database/config.ts | 80 +-- src/core/database/containerStats.ts | 46 +- src/core/database/database.ts | 47 +- src/core/database/dockerHosts.ts | 90 +-- src/core/database/helper.ts | 40 +- src/core/database/hostStats.ts | 38 +- src/core/database/index.ts | 18 +- src/core/database/logs.ts | 108 +-- src/core/database/stacks.ts | 84 +-- src/core/docker/client.ts | 52 +- src/core/docker/monitor.ts | 236 +++--- src/core/docker/scheduler.ts | 180 ++--- src/core/docker/store-container-stats.ts | 166 ++--- src/core/docker/store-host-stats.ts | 132 ++-- src/core/plugins/loader.ts | 98 +-- src/core/plugins/plugin-manager.ts | 228 +++--- src/core/stacks/controller.ts | 555 ++++++++------- src/core/utils/calculations.ts | 50 +- src/core/utils/change-me-checker.ts | 26 +- src/core/utils/helpers.ts | 12 +- src/core/utils/logger.ts | 300 ++++---- src/core/utils/package-json.ts | 24 +- src/core/utils/response-handler.ts | 63 +- src/index.ts | 282 ++++---- src/middleware/auth.ts | 120 ++-- src/plugins/example.plugin.ts | 176 ++--- src/plugins/telegram.plugin.ts | 48 +- src/routes/api-config.ts | 374 ++++++---- src/routes/docker-manager.ts | 218 +++--- src/routes/docker-stats.ts | 294 ++++---- src/routes/docker-websocket.ts | 220 +++--- src/routes/live-logs.ts | 37 +- src/routes/live-stacks.ts | 37 +- src/routes/logs.ts | 174 ++--- src/routes/stacks.ts | 569 ++++++++------- src/routes/utils.ts | 80 +-- src/tests/cleanup.ts | 8 +- src/tests/delete.spec.ts | 12 +- src/tests/gets.spec.ts | 106 +-- src/tests/helper.ts | 215 +++--- src/tests/post.spec.ts | 92 +-- src/typings/database.ts | 34 +- src/typings/docker-compose.ts | 868 +++++++++++++---------- src/typings/docker.ts | 60 +- src/typings/dockerode.ts | 316 ++++----- src/typings/elysiajs.ts | 12 +- src/typings/misc.ts | 5 + src/typings/plugin.ts | 38 +- src/typings/websocket.ts | 18 +- tsconfig.json | 196 ++--- 58 files changed, 4047 insertions(+), 3569 deletions(-) create mode 100644 biome.json create mode 100644 src/core/database/_dbState.ts create mode 100644 src/core/database/backup.ts create mode 100644 src/typings/misc.ts diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index c4ddc1f3..4a4f895f 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -23,6 +23,18 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Lint + run: | + bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --reporter=github --fix src + + - name: Commit and Push Changes + uses: EndBug/add-and-commit@v9 + with: + add: "src" + message: "Linting" + committer_name: "GitHub Action" + committer_email: "action@github.com" + - name: Start proxy run: | docker compose -f docker/docker-compose.dev.yaml up -d diff --git a/.gitignore b/.gitignore index 322656b4..527c7b3f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /node_modules .test dependency-graph* -build \ No newline at end of file +build +data \ No newline at end of file diff --git a/.knip.json b/.knip.json index 0c1cd545..e786d748 100644 --- a/.knip.json +++ b/.knip.json @@ -1,5 +1,5 @@ { - "entry": ["src/index.ts"], - "project": ["src/**/*.ts"], - "ignore": ["src/plugins/*.plugin.ts","src/tests/*.ts"] + "entry": ["src/index.ts"], + "project": ["src/**/*.ts"], + "ignore": ["src/plugins/*.plugin.ts", "src/tests/*.ts"] } diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..a02c1a30 --- /dev/null +++ b/biome.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": false + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/bun.lock b/bun.lock index ffa2cdf1..ad696a27 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,7 @@ "yaml": "^2.7.1", }, "devDependencies": { + "@biomejs/biome": "1.9.4", "@types/dockerode": "^3.3.38", "@types/node": "^22.14.1", "@types/split2": "^4.2.3", @@ -34,6 +35,24 @@ "packages": { "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], @@ -368,6 +387,8 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], "easy-table/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/package.json b/package.json index b29b98d6..8b64f69f 100644 --- a/package.json +++ b/package.json @@ -1,51 +1,51 @@ { - "name": "dockstatapi", - "author": { - "email": "info@itsnik.de", - "name": "ItsNik", - "url": "https://github.com/Its4Nik" - }, - "license": "CC BY-NC 4.0", - "contributors": [], - "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", - "version": "3.0.0", - "scripts": { - "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", - "start:docker": "bun run build:docker && docker run -p 3000:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", - "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", - "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", - "build": "bun build --target bun src/index.ts --outdir ./dist", - "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", - "clean": "bun run clean:win || bun run clean:lin", - "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q dockstatapi.db* && echo 'success'", - "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f dockstatapi.db* && echo 'success'", - "knip": "knip" - }, - "dependencies": { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - "@elysiajs/swagger": "^1.2.2", - "chalk": "^5.4.1", - "docker-compose": "^1.2.0", - "dockerode": "^4.0.5", - "elysia": "latest", - "knip": "latest", - "split2": "^4.2.0", - "winston": "^3.17.0", - "yaml": "^2.7.1" - }, - "devDependencies": { - "@types/dockerode": "^3.3.38", - "@types/node": "^22.14.1", - "@types/split2": "^4.2.3", - "bun-types": "latest", - "cross-env": "^7.0.3", - "logform": "^2.7.0", - "typescript": "^5.8.3", - "wrap-ansi": "^9.0.0" - }, - "module": "src/index.js", - "trustedDependencies": [ - "protobufjs" - ] + "name": "dockstatapi", + "author": { + "email": "info@itsnik.de", + "name": "ItsNik", + "url": "https://github.com/Its4Nik" + }, + "license": "CC BY-NC 4.0", + "contributors": [], + "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", + "version": "3.0.0", + "scripts": { + "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", + "start:docker": "bun run build:docker && docker run -p 3000:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", + "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", + "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", + "build": "bun build --target bun src/index.ts --outdir ./dist", + "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", + "clean": "bun run clean:win || bun run clean:lin", + "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi*.db* && echo 'success'", + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi*.db* && echo 'success'", + "knip": "knip", + "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src" + }, + "dependencies": { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + "@elysiajs/swagger": "^1.2.2", + "chalk": "^5.4.1", + "docker-compose": "^1.2.0", + "dockerode": "^4.0.5", + "elysia": "latest", + "knip": "latest", + "split2": "^4.2.0", + "winston": "^3.17.0", + "yaml": "^2.7.1" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + "@types/node": "^22.14.1", + "@types/split2": "^4.2.3", + "bun-types": "latest", + "cross-env": "^7.0.3", + "logform": "^2.7.0", + "typescript": "^5.8.3", + "wrap-ansi": "^9.0.0" + }, + "module": "src/index.js", + "trustedDependencies": ["protobufjs"] } diff --git a/src/core/database/_dbState.ts b/src/core/database/_dbState.ts new file mode 100644 index 00000000..e159ca05 --- /dev/null +++ b/src/core/database/_dbState.ts @@ -0,0 +1,5 @@ +export let backupInProgress = false; + +export function setBackupInProgress(val: boolean) { + backupInProgress = val; +} diff --git a/src/core/database/backup.ts b/src/core/database/backup.ts new file mode 100644 index 00000000..4efa130c --- /dev/null +++ b/src/core/database/backup.ts @@ -0,0 +1,163 @@ +import { copyFileSync, existsSync, readdirSync } from "node:fs"; +import { logger } from "~/core/utils/logger"; +import type { BackupInfo } from "~/typings/misc"; +import { backupInProgress, setBackupInProgress } from "./_dbState"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +export const backupDir = "data/"; + +export async function backupDatabase(): Promise { + if (backupInProgress) { + logger.error("Backup attempt blocked: Another backup already in progress"); + throw new Error("Backup already in progress"); + } + + logger.debug("Starting database backup process..."); + setBackupInProgress(true); + + try { + logger.debug("Executing WAL checkpoint..."); + db.exec("PRAGMA wal_checkpoint(FULL);"); + logger.debug("WAL checkpoint completed successfully"); + + const now = new Date(); + const day = String(now.getDate()).padStart(2, "0"); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const year = now.getFullYear(); + const dateStr = `${day}-${month}-${year}`; + logger.debug(`Using date string for backup: ${dateStr}`); + + logger.debug(`Scanning backup directory: ${backupDir}`); + const files = readdirSync(backupDir); + logger.debug(`Found ${files.length} files in backup directory`); + + const regex = new RegExp( + `^dockstatapi-${day}-${month}-${year}-(\\d+)\\.db\\.bak$`, + ); + let maxBackupNum = 0; + + for (const file of files) { + const match = file.match(regex); + if (match?.[1]) { + const num = Number.parseInt(match[1], 10); + logger.debug(`Found existing backup file: ${file} with number ${num}`); + if (num > maxBackupNum) { + maxBackupNum = num; + } + } else { + logger.debug(`Skipping non-matching file: ${file}`); + } + } + + logger.debug(`Maximum backup number found: ${maxBackupNum}`); + const backupNumber = maxBackupNum + 1; + const backupFilename = `${backupDir}dockstatapi-${dateStr}-${backupNumber}.db.bak`; + logger.debug(`Generated backup filename: ${backupFilename}`); + + logger.debug(`Attempting to copy database to ${backupFilename}`); + try { + copyFileSync(`${backupDir}dockstatapi.db`, backupFilename); + logger.info(`Backup created successfully: ${backupFilename}`); + logger.debug("File copy operation completed without errors"); + } catch (e) { + logger.error(`Failed to create backup file: ${(e as Error).message}`); + throw e; + } + + return backupFilename; + } finally { + setBackupInProgress(false); + logger.debug("Backup process completed, in progress flag reset"); + } +} + +export function restoreDatabase(backupFilename: string): void { + const backupFile = `${backupDir}${backupFilename}`; + + if (backupInProgress) { + logger.error("Restore attempt blocked: Backup in progress"); + throw new Error("Backup in progress. Cannot restore."); + } + + logger.debug(`Starting database restore from ${backupFile}`); + + if (!existsSync(backupFile)) { + logger.error(`Backup file not found: ${backupFile}`); + throw new Error(`Backup file ${backupFile} does not exist.`); + } + + setBackupInProgress(true); + try { + executeDbOperation( + "restore", + () => { + logger.debug(`Attempting to restore database from ${backupFile}`); + try { + copyFileSync(backupFile, `${backupDir}dockstatapi.db`); + logger.info(`Database restored successfully from: ${backupFilename}`); + logger.debug("Database file replacement completed"); + } catch (e) { + logger.error(`Restore failed: ${(e as Error).message}`); + throw e; + } + }, + () => { + if (backupInProgress) { + logger.error("Database operation attempted during restore"); + throw new Error("Cannot perform database operations during restore"); + } + }, + ); + } finally { + setBackupInProgress(false); + logger.debug("Restore process completed, in progress flag reset"); + } +} + +export const findLatestBackup = (): string => { + logger.debug(`Searching for latest backup in directory: ${backupDir}`); + + const files = readdirSync(backupDir); + logger.debug(`Found ${files.length} files to process`); + + const backups = files + .map((file): BackupInfo | null => { + const match = file.match( + /^dockstatapi-(\d{2})-(\d{2})-(\d{4})-(\d+)\.db\.bak$/, + ); + if (!match) { + logger.debug(`Skipping non-backup file: ${file}`); + return null; + } + + const date = new Date( + Number(match[3]), + Number(match[2]) - 1, + Number(match[1]), + ); + logger.debug( + `Found backup file: ${file} with date ${date.toISOString()}`, + ); + + return { + filename: file, + date, + backupNum: Number(match[4]), + }; + }) + .filter((backup): backup is BackupInfo => backup !== null) + .sort((a, b) => { + const dateDiff = b.date.getTime() - a.date.getTime(); + return dateDiff !== 0 ? dateDiff : b.backupNum - a.backupNum; + }); + + if (!backups.length) { + logger.error("No valid backup files found"); + throw new Error("No backups available"); + } + + const latestBackup = backups[0].filename; + logger.debug(`Determined latest backup file: ${latestBackup}`); + return latestBackup; +}; diff --git a/src/core/database/config.ts b/src/core/database/config.ts index 126682e5..f2460e06 100644 --- a/src/core/database/config.ts +++ b/src/core/database/config.ts @@ -2,54 +2,54 @@ import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - update: db.prepare( - `UPDATE config SET fetching_interval = ?, keep_data_for = ?, api_key = ?`, - ), - select: db.prepare( - `SELECT keep_data_for, fetching_interval, api_key FROM config`, - ), - deleteOld: db.prepare( - `DELETE FROM container_stats WHERE timestamp < datetime('now', '-' || ? || ' days')`, - ), - deleteOldLogs: db.prepare( - `DELETE FROM backend_log_entries WHERE timestamp < datetime('now', '-' || ? || ' days')`, - ), + update: db.prepare( + "UPDATE config SET fetching_interval = ?, keep_data_for = ?, api_key = ?", + ), + select: db.prepare( + "SELECT keep_data_for, fetching_interval, api_key FROM config", + ), + deleteOld: db.prepare( + `DELETE FROM container_stats WHERE timestamp < datetime('now', '-' || ? || ' days')`, + ), + deleteOldLogs: db.prepare( + `DELETE FROM backend_log_entries WHERE timestamp < datetime('now', '-' || ? || ' days')`, + ), }; export function updateConfig( - fetching_interval: number, - keep_data_for: number, - api_key: string, + fetching_interval: number, + keep_data_for: number, + api_key: string, ) { - return executeDbOperation( - "Update Config", - () => stmt.update.run(fetching_interval, keep_data_for, api_key), - () => { - if ( - typeof fetching_interval !== "number" || - typeof keep_data_for !== "number" - ) { - throw new TypeError("Invalid config parameters"); - } - }, - ); + return executeDbOperation( + "Update Config", + () => stmt.update.run(fetching_interval, keep_data_for, api_key), + () => { + if ( + typeof fetching_interval !== "number" || + typeof keep_data_for !== "number" + ) { + throw new TypeError("Invalid config parameters"); + } + }, + ); } export function getConfig() { - return executeDbOperation("Get Config", () => stmt.select.all()); + return executeDbOperation("Get Config", () => stmt.select.all()); } export function deleteOldData(days: number) { - return executeDbOperation( - "Delete Old Data", - () => { - db.transaction(() => { - stmt.deleteOld.run(days); - stmt.deleteOldLogs.run(days); - })(); - }, - () => { - if (typeof days !== "number") throw new TypeError("Invalid days type"); - }, - ); + return executeDbOperation( + "Delete Old Data", + () => { + db.transaction(() => { + stmt.deleteOld.run(days); + stmt.deleteOldLogs.run(days); + })(); + }, + () => { + if (typeof days !== "number") throw new TypeError("Invalid days type"); + }, + ); } diff --git a/src/core/database/containerStats.ts b/src/core/database/containerStats.ts index d0fb1970..a5d6bcf0 100644 --- a/src/core/database/containerStats.ts +++ b/src/core/database/containerStats.ts @@ -7,28 +7,28 @@ const stmt = db.prepare(` `); export function addContainerStats( - id: string, - hostId: string, - name: string, - image: string, - status: string, - state: string, - cpu_usage: number, - memory_usage: number, + id: string, + hostId: string, + name: string, + image: string, + status: string, + state: string, + cpu_usage: number, + memory_usage: number, ) { - return executeDbOperation( - "Add Container Stats", - () => - stmt.run(id, hostId, name, image, status, state, cpu_usage, memory_usage), - () => { - if ( - typeof id !== "string" || - typeof hostId !== "string" || - typeof cpu_usage !== "number" || - typeof memory_usage !== "number" - ) { - throw new TypeError("Invalid container stats parameters"); - } - }, - ); + return executeDbOperation( + "Add Container Stats", + () => + stmt.run(id, hostId, name, image, status, state, cpu_usage, memory_usage), + () => { + if ( + typeof id !== "string" || + typeof hostId !== "string" || + typeof cpu_usage !== "number" || + typeof memory_usage !== "number" + ) { + throw new TypeError("Invalid container stats parameters"); + } + }, + ); } diff --git a/src/core/database/database.ts b/src/core/database/database.ts index 5173e448..db33ec9c 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -1,10 +1,19 @@ import { Database } from "bun:sqlite"; -export const db = new Database("dockstatapi.db", { strict: true }); +import { existsSync, mkdirSync } from "node:fs"; + +const dataFolder = "data"; +if (!existsSync(dataFolder)) { + mkdirSync(dataFolder, { recursive: true }); +} + +export const databasePath = "data/dockstatapi.db"; +export const db = new Database(databasePath, { strict: true }); + db.exec("PRAGMA journal_mode = WAL;"); export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -57,7 +66,7 @@ export function init() { status TEXT NOT NULL, state TEXT NOT NULL, cpu_usage FLOAT NOT NULL, - memory_usage FLOAT NOT NULL, + memory_usage, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP ); @@ -68,25 +77,25 @@ export function init() { ); `); - const configRow = db - .prepare(`SELECT COUNT(*) AS count FROM config`) - .get() as { count: number }; + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; - if (configRow.count === 0) { - db.prepare( - `INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")`, - ).run(); - } + if (configRow.count === 0) { + db.prepare( + 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")', + ).run(); + } - const hostRow = db - .prepare(`SELECT COUNT(*) AS count FROM docker_hosts`) - .get() as { count: number }; + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; - if (hostRow.count === 0) { - db.prepare( - `INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)`, - ).run("Localhost", "localhost:2375", false); - } + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ).run("Localhost", "localhost:2375", false); + } } init(); diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts index c8057030..18180c54 100644 --- a/src/core/database/dockerHosts.ts +++ b/src/core/database/dockerHosts.ts @@ -1,62 +1,62 @@ +import type { DockerHost } from "~/typings/docker"; import { db } from "./database"; import { executeDbOperation } from "./helper"; -import type { DockerHost } from "~/typings/docker"; const stmt = { - insert: db.prepare( - `INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)`, - ), - selectAll: db.prepare( - `SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC`, - ), - update: db.prepare( - `UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?`, - ), - delete: db.prepare(`DELETE FROM docker_hosts WHERE id = ?`), + insert: db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ), + selectAll: db.prepare( + "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC", + ), + update: db.prepare( + "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?", + ), + delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), }; export function addDockerHost(host: DockerHost) { - return executeDbOperation( - "Add Docker Host", - () => stmt.insert.run(host.name, host.hostAddress, host.secure), - () => { - if (!host.name || !host.hostAddress) - throw new Error("Missing required fields"); - if (typeof host.secure !== "boolean") - throw new TypeError("Invalid secure type"); - }, - ); + return executeDbOperation( + "Add Docker Host", + () => stmt.insert.run(host.name, host.hostAddress, host.secure), + () => { + if (!host.name || !host.hostAddress) + throw new Error("Missing required fields"); + if (typeof host.secure !== "boolean") + throw new TypeError("Invalid secure type"); + }, + ); } export function getDockerHosts(): DockerHost[] { - return executeDbOperation("Get Docker Hosts", () => { - const rows = stmt.selectAll.all() as Array< - Omit & { secure: number } - >; - return rows.map((row) => ({ - ...row, - secure: row.secure === 1, - })); - }); + return executeDbOperation("Get Docker Hosts", () => { + const rows = stmt.selectAll.all() as Array< + Omit & { secure: number } + >; + return rows.map((row) => ({ + ...row, + secure: row.secure === 1, + })); + }); } 1; export function updateDockerHost(host: DockerHost) { - return executeDbOperation( - "Update Docker Host", - () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), - () => { - if (!host.id || typeof host.id !== "number") - throw new Error("Invalid host ID"); - }, - ); + return executeDbOperation( + "Update Docker Host", + () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), + () => { + if (!host.id || typeof host.id !== "number") + throw new Error("Invalid host ID"); + }, + ); } export function deleteDockerHost(id: number) { - return executeDbOperation( - "Delete Docker Host", - () => stmt.delete.run(id), - () => { - if (typeof id !== "number") throw new TypeError("Invalid ID type"); - }, - ); + return executeDbOperation( + "Delete Docker Host", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid ID type"); + }, + ); } diff --git a/src/core/database/helper.ts b/src/core/database/helper.ts index 3edbdaa8..1f1cabd9 100644 --- a/src/core/database/helper.ts +++ b/src/core/database/helper.ts @@ -1,22 +1,28 @@ import { logger } from "~/core/utils/logger"; +import { backupInProgress } from "./_dbState"; export function executeDbOperation( - label: string, - operation: () => T, - validate?: () => void, - dontLog?: boolean, + label: string, + operation: () => T, + validate?: () => void, + dontLog?: boolean, ): T { - const startTime = Date.now(); - if (dontLog !== true) { - logger.debug(`__task__ __db__ ${label} ⏳`); - } - if (validate) { - validate(); - } - const result = operation(); - const duration = Date.now() - startTime; - if (dontLog !== true) { - logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); - } - return result; + if (backupInProgress && label !== "backup" && label !== "restore") { + throw new Error( + `backup in progress Database operation not allowed: ${label}`, + ); + } + const startTime = Date.now(); + if (dontLog !== true) { + logger.debug(`__task__ __db__ ${label} ⏳`); + } + if (validate) { + validate(); + } + const result = operation(); + const duration = Date.now() - startTime; + if (dontLog !== true) { + logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); + } + return result; } diff --git a/src/core/database/hostStats.ts b/src/core/database/hostStats.ts index 04aa426e..3d48528d 100644 --- a/src/core/database/hostStats.ts +++ b/src/core/database/hostStats.ts @@ -1,6 +1,6 @@ +import type { HostStats } from "~/typings/docker"; import { db } from "./database"; import { executeDbOperation } from "./helper"; -import type { HostStats } from "~/typings/docker"; const stmt = db.prepare(` INSERT INTO host_stats ( @@ -24,22 +24,22 @@ const stmt = db.prepare(` `); export function updateHostStats(stats: HostStats) { - return executeDbOperation("Update Host Stats", () => - stmt.run( - stats.hostId, - stats.hostName, - stats.dockerVersion, - stats.apiVersion, - stats.os, - stats.architecture, - stats.totalMemory, - stats.totalCPU, - JSON.stringify(stats.labels), - stats.containers, - stats.containersRunning, - stats.containersStopped, - stats.containersPaused, - stats.images, - ), - ); + return executeDbOperation("Update Host Stats", () => + stmt.run( + stats.hostId, + stats.hostName, + stats.dockerVersion, + stats.apiVersion, + stats.os, + stats.architecture, + stats.totalMemory, + stats.totalCPU, + JSON.stringify(stats.labels), + stats.containers, + stats.containersRunning, + stats.containersStopped, + stats.containersPaused, + stats.images, + ), + ); } diff --git a/src/core/database/index.ts b/src/core/database/index.ts index 3559fe94..9158cadf 100644 --- a/src/core/database/index.ts +++ b/src/core/database/index.ts @@ -2,18 +2,20 @@ import { init } from "~/core/database/database"; init(); -import * as dockerHosts from "~/core/database/dockerHosts"; -import * as logs from "~/core/database/logs"; +import * as backup from "~/core/database/backup"; import * as config from "~/core/database/config"; import * as containerStats from "~/core/database/containerStats"; +import * as dockerHosts from "~/core/database/dockerHosts"; import * as hostStats from "~/core/database/hostStats"; +import * as logs from "~/core/database/logs"; import * as stacks from "~/core/database/stacks"; export const dbFunctions = { - ...dockerHosts, - ...logs, - ...config, - ...containerStats, - ...hostStats, - ...stacks, + ...dockerHosts, + ...logs, + ...config, + ...containerStats, + ...hostStats, + ...stacks, + ...backup, }; diff --git a/src/core/database/logs.ts b/src/core/database/logs.ts index 26ce2c1c..3fba8dcc 100644 --- a/src/core/database/logs.ts +++ b/src/core/database/logs.ts @@ -1,73 +1,73 @@ -import { logStreamData } from "~/typings/websocket"; +import type { log_message } from "~/typings/database"; import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare( - `INSERT INTO backend_log_entries (timestamp, level, message, file, line) VALUES (?, ?, ?, ?, ?)`, - ), - selectAll: db.prepare( - `SELECT timestamp, level, message, file, line FROM backend_log_entries ORDER BY timestamp DESC`, - ), - selectByLevel: db.prepare( - `SELECT timestamp, level, message, file, line FROM backend_log_entries WHERE level = ?`, - ), - deleteAll: db.prepare(`DELETE FROM backend_log_entries`), - deleteByLevel: db.prepare(`DELETE FROM backend_log_entries WHERE level = ?`), + insert: db.prepare( + "INSERT INTO backend_log_entries (timestamp, level, message, file, line) VALUES (?, ?, ?, ?, ?)", + ), + selectAll: db.prepare( + "SELECT timestamp, level, message, file, line FROM backend_log_entries ORDER BY timestamp DESC", + ), + selectByLevel: db.prepare( + "SELECT timestamp, level, message, file, line FROM backend_log_entries WHERE level = ?", + ), + deleteAll: db.prepare("DELETE FROM backend_log_entries"), + deleteByLevel: db.prepare("DELETE FROM backend_log_entries WHERE level = ?"), }; -export function addLogEntry(data: logStreamData) { - return executeDbOperation( - "Add Log Entry", - () => - stmt.insert.run( - data.level, - data.timestamp, - data.message, - data.file, - data.line, - ), - () => { - if ( - typeof data.level !== "string" || - typeof data.timestamp !== "string" || - typeof data.message !== "string" || - typeof data.file !== "string" || - typeof data.line !== "number" - ) { - throw new TypeError( - `Invalid log entry parameters ${data.file} ${data.line} ${data.message} ${data}`, - ); - } - }, - true, - ); +export function addLogEntry(data: log_message) { + return executeDbOperation( + "Add Log Entry", + () => + stmt.insert.run( + data.level, + data.timestamp, + data.message, + data.file, + data.line, + ), + () => { + if ( + typeof data.level !== "string" || + typeof data.timestamp !== "string" || + typeof data.message !== "string" || + typeof data.file !== "string" || + typeof data.line !== "number" + ) { + throw new TypeError( + "Invalid log entry parameters ${data.file} ${data.line} ${data.message} ${data}", + ); + } + }, + true, + ); } export function getAllLogs() { - return executeDbOperation("Get All Logs", () => stmt.selectAll.all()); + return executeDbOperation("Get All Logs", () => stmt.selectAll.all()); } export function getLogsByLevel(level: string) { - return executeDbOperation( - "Get Logs By Level", - () => stmt.selectByLevel.all(level), - () => { - if (typeof level !== "string") throw new TypeError("Invalid level type"); - }, - ); + return executeDbOperation( + "Get Logs By Level", + () => stmt.selectByLevel.all(level), + () => { + if (typeof level !== "string") throw new TypeError("Invalid level type"); + }, + ); } export function clearAllLogs() { - return executeDbOperation("Clear All Logs", () => stmt.deleteAll.run()); + return executeDbOperation("Clear All Logs", () => stmt.deleteAll.run()); } export function clearLogsByLevel(level: string) { - return executeDbOperation( - "Clear Logs By Level", - () => stmt.deleteByLevel.run(level), - () => { - if (typeof level !== "string") throw new TypeError("Invalid level type"); - }, - ); + return executeDbOperation( + "Clear Logs By Level", + () => stmt.deleteByLevel.run(level), + () => { + if (typeof level !== "string") throw new TypeError("Invalid level type"); + }, + ); } diff --git a/src/core/database/stacks.ts b/src/core/database/stacks.ts index 18aa1160..f39d01a6 100644 --- a/src/core/database/stacks.ts +++ b/src/core/database/stacks.ts @@ -1,75 +1,75 @@ -import { Stack } from "~/typings/docker-compose"; -import { db } from "./database"; -import { executeDbOperation } from "./helper"; import type { stacks_config } from "~/typings/database"; +import type { Stack } from "~/typings/docker-compose"; import { findObjectByKey } from "../utils/helpers"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare(` + insert: db.prepare(` INSERT INTO stacks_config ( name, version, custom, source, container_count, stack_prefix, automatic_reboot_on_error, image_updates ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `), - selectAll: db.prepare(` + selectAll: db.prepare(` SELECT id, name, version, custom, source, container_count, stack_prefix, automatic_reboot_on_error, image_updates FROM stacks_config ORDER BY name DESC `), - update: db.prepare(` + update: db.prepare(` UPDATE stacks_config SET version = ?, custom = ?, source = ?, container_count = ?, stack_prefix = ?, automatic_reboot_on_error = ?, image_updates = ? WHERE name = ? `), - delete: db.prepare(`DELETE FROM stacks_config WHERE id = ?`), + delete: db.prepare("DELETE FROM stacks_config WHERE id = ?"), }; export function addStack(stack: stacks_config) { - executeDbOperation("Add Stack", () => - stmt.insert.run( - stack.name, - stack.version, - stack.custom, - stack.source, - stack.container_count, - stack.stack_prefix, - stack.automatic_reboot_on_error, - stack.image_updates - ) - ); + executeDbOperation("Add Stack", () => + stmt.insert.run( + stack.name, + stack.version, + stack.custom, + stack.source, + stack.container_count, + stack.stack_prefix, + stack.automatic_reboot_on_error, + stack.image_updates, + ), + ); - return findObjectByKey(getStacks(), "name", stack.name)?.id; + return findObjectByKey(getStacks(), "name", stack.name)?.id; } export function getStacks() { - return executeDbOperation("Get Stacks", () => - stmt.selectAll.all() - ) as Stack[]; + return executeDbOperation("Get Stacks", () => + stmt.selectAll.all(), + ) as Stack[]; } export function deleteStack(id: number) { - return executeDbOperation( - "Delete Stack", - () => stmt.delete.run(id), - () => { - if (typeof id !== "number") throw new TypeError("Invalid stack ID"); - } - ); + return executeDbOperation( + "Delete Stack", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid stack ID"); + }, + ); } export function updateStack(stack: stacks_config) { - return executeDbOperation("Update Stack", () => - stmt.update.run( - stack.version, - stack.custom, - stack.source, - stack.container_count, - stack.stack_prefix, - stack.automatic_reboot_on_error, - stack.image_updates, - stack.name - ) - ); + return executeDbOperation("Update Stack", () => + stmt.update.run( + stack.version, + stack.custom, + stack.source, + stack.container_count, + stack.stack_prefix, + stack.automatic_reboot_on_error, + stack.image_updates, + stack.name, + ), + ); } diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index 7d8e6ea4..ad65540b 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -1,33 +1,33 @@ -import type { DockerHost } from "~/typings/docker"; import Docker from "dockerode"; import { logger } from "~/core/utils/logger"; +import type { DockerHost } from "~/typings/docker"; export const getDockerClient = (host: DockerHost): Docker => { - try { - const inputUrl = host.hostAddress.includes("://") - ? host.hostAddress - : `${host.secure ? "https" : "http"}://${host.hostAddress}`; - const parsedUrl = new URL(inputUrl); - const hostAddress = parsedUrl.hostname; - let port = parsedUrl.port - ? parseInt(parsedUrl.port) - : host.secure - ? 2376 - : 2375; + try { + const inputUrl = host.hostAddress.includes("://") + ? host.hostAddress + : `${host.secure ? "https" : "http"}://${host.hostAddress}`; + const parsedUrl = new URL(inputUrl); + const hostAddress = parsedUrl.hostname; + const port = parsedUrl.port + ? Number.parseInt(parsedUrl.port) + : host.secure + ? 2376 + : 2375; - if (isNaN(port) || port < 1 || port > 65535) { - throw new Error("Invalid port number in Docker host URL"); - } + if (Number.isNaN(port) || port < 1 || port > 65535) { + throw new Error("Invalid port number in Docker host URL"); + } - return new Docker({ - protocol: host.secure ? "https" : "http", - host: hostAddress, - port, - version: "v1.41", - // TODO: Add TLS configuration if needed - }); - } catch (error) { - logger.error("Invalid Docker host URL configuration:", error); - throw new Error("Invalid Docker host configuration"); - } + return new Docker({ + protocol: host.secure ? "https" : "http", + host: hostAddress, + port, + version: "v1.41", + // TODO: Add TLS configuration if needed + }); + } catch (error) { + logger.error("Invalid Docker host URL configuration:", error); + throw new Error("Invalid Docker host configuration"); + } }; diff --git a/src/core/docker/monitor.ts b/src/core/docker/monitor.ts index eadd5f62..d10c3c65 100644 --- a/src/core/docker/monitor.ts +++ b/src/core/docker/monitor.ts @@ -1,138 +1,142 @@ -import type { DockerHost } from "~/typings/docker"; +import { sleep } from "bun"; +import Docker from "dockerode"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { logger } from "~/core/utils/logger"; +import type { DockerHost } from "~/typings/docker"; +import type { ContainerInfo } from "~/typings/docker"; import { pluginManager } from "../plugins/plugin-manager"; -import { ContainerInfo } from "~/typings/docker"; -import { sleep } from "bun"; export async function monitorDockerEvents() { - let hosts: DockerHost[]; + let hosts: DockerHost[]; - try { - hosts = dbFunctions.getDockerHosts(); - logger.debug( - `Retrieved ${hosts.length} Docker host(s) for event monitoring.`, - ); - } catch (error: unknown) { - logger.error(`Error retrieving Docker hosts: ${(error as Error).message}`); - return; - } + try { + hosts = dbFunctions.getDockerHosts(); + logger.debug( + `Retrieved ${hosts.length} Docker host(s) for event monitoring.`, + ); + } catch (error: unknown) { + logger.error(`Error retrieving Docker hosts: ${(error as Error).message}`); + return; + } - for (const host of hosts) { - await startFor(host); - } + for (const host of hosts) { + await startFor(host); + } } async function startFor(host: DockerHost) { - const docker = getDockerClient(host); - try { - await docker.ping(); - pluginManager.handleHostReachableAgain(host.name); - } catch (err: any) { - logger.warn(`Restarting Stream for ${host.name} in 10 seconds...`); - pluginManager.handleHostUnreachable(host.name, err); - await sleep(10000); - startFor(host); - } + const docker = getDockerClient(host); + try { + await docker.ping(); + pluginManager.handleHostReachableAgain(host.name); + } catch (err) { + logger.warn(`Restarting Stream for ${host.name} in 10 seconds...`); + pluginManager.handleHostUnreachable(host.name, String(err)); + await sleep(10000); + startFor(host); + } - try { - const eventsStream = await docker.getEvents(); - logger.debug(`Started events stream for host: ${host.name}`); + try { + const eventsStream = await docker.getEvents(); + logger.debug(`Started events stream for host: ${host.name}`); - let buffer = ""; + let buffer = ""; - eventsStream.on("data", (chunk: Buffer) => { - buffer += chunk.toString("utf8"); - const lines = buffer.split(/\r?\n/); + eventsStream.on("data", (chunk: Buffer) => { + buffer += chunk.toString("utf8"); + const lines = buffer.split(/\r?\n/); - buffer = lines.pop() || ""; + buffer = lines.pop() || ""; - for (const line of lines) { - if (line.trim() === "") { - continue; - } + for (const line of lines) { + if (line.trim() === "") { + continue; + } - let event: any; - try { - event = JSON.parse(line); - } catch (parseErr: any) { - logger.error( - `Failed to parse event from host ${host.name}: ${parseErr.message}`, - ); - continue; - } + //biome-ignore lint/suspicious/noExplicitAny: Unsure what data we are receiving here + let event: any; + try { + event = JSON.parse(line); + } catch (parseErr) { + logger.error( + `Failed to parse event from host ${host.name}: ${String(parseErr)}`, + ); + continue; + } - if (event.Type === "container") { - const containerInfo: ContainerInfo = { - id: event.Actor?.ID || event.id || "", - hostId: host.name, - name: event.Actor?.Attributes?.name || "", - image: event.Actor?.Attributes?.image || event.from || "", - status: event.status || event.Actor?.Attributes?.status || "", - state: event.Actor?.Attributes?.state || event.Action || "", - cpuUsage: 0, - memoryUsage: 0, - }; + if (event.Type === "container") { + const containerInfo: ContainerInfo = { + id: event.Actor?.ID || event.id || "", + hostId: host.name, + name: event.Actor?.Attributes?.name || "", + image: event.Actor?.Attributes?.image || event.from || "", + status: event.status || event.Actor?.Attributes?.status || "", + state: event.Actor?.Attributes?.state || event.Action || "", + cpuUsage: 0, + memoryUsage: 0, + }; - const action = event.Action; - logger.debug(`Triggering Action [${action}]`); - switch (action) { - case "stop": - pluginManager.handleContainerStop(containerInfo); - break; - case "start": - pluginManager.handleContainerStart(containerInfo); - break; - case "die": - pluginManager.handleContainerDie(containerInfo); - break; - case "kill": - pluginManager.handleContainerKill(containerInfo); - break; - case "create": - pluginManager.handleContainerCreate(containerInfo); - break; - case "destroy": - pluginManager.handleContainerDestroy(containerInfo); - break; - case "pause": - pluginManager.handleContainerPause(containerInfo); - break; - case "unpause": - pluginManager.handleContainerUnpause(containerInfo); - break; - case "restart": - pluginManager.handleContainerRestart(containerInfo); - break; - case "update": - pluginManager.handleContainerUpdate(containerInfo); - break; - case "health_status": - pluginManager.handleContainerHealthStatus(containerInfo); - break; - default: - logger.debug( - `Unhandled container event "${action}" on host ${host.name}`, - ); - } - } - } - }); + const action = event.Action; + logger.debug(`Triggering Action [${action}]`); + switch (action) { + case "stop": + pluginManager.handleContainerStop(containerInfo); + break; + case "start": + pluginManager.handleContainerStart(containerInfo); + break; + case "die": + pluginManager.handleContainerDie(containerInfo); + break; + case "kill": + pluginManager.handleContainerKill(containerInfo); + break; + case "create": + pluginManager.handleContainerCreate(containerInfo); + break; + case "destroy": + pluginManager.handleContainerDestroy(containerInfo); + break; + case "pause": + pluginManager.handleContainerPause(containerInfo); + break; + case "unpause": + pluginManager.handleContainerUnpause(containerInfo); + break; + case "restart": + pluginManager.handleContainerRestart(containerInfo); + break; + case "update": + pluginManager.handleContainerUpdate(containerInfo); + break; + case "health_status": + pluginManager.handleContainerHealthStatus(containerInfo); + break; + default: + logger.debug( + `Unhandled container event "${action}" on host ${host.name}`, + ); + } + } + } + }); - eventsStream.on("error", async (err: Error) => { - logger.error(`Events stream error for host ${host.name}: ${err.message}`); - logger.warn(`Restarting Stream for ${host.name} in 10 seconds...`); - await sleep(10000); - startFor(host); - }); + eventsStream.on("error", async (err: Error) => { + logger.error(`Events stream error for host ${host.name}: ${err.message}`); + logger.warn(`Restarting Stream for ${host.name} in 10 seconds...`); + await sleep(10000); + startFor(host); + }); - eventsStream.on("end", () => { - logger.info(`Events stream ended for host ${host.name}`); - }); - } catch (streamErr: any) { - logger.error( - `Failed to start events stream for host ${host.name}: ${streamErr.message}`, - ); - } + eventsStream.on("end", () => { + logger.info(`Events stream ended for host ${host.name}`); + }); + } catch (streamErr) { + logger.error( + `Failed to start events stream for host ${host.name}: ${String( + streamErr, + )}`, + ); + } } diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index d1dd1248..8682411b 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -1,115 +1,115 @@ -import storeContainerData from "~/core/docker/store-container-stats"; import { dbFunctions } from "~/core/database"; -import { config } from "~/typings/database"; -import { logger } from "~/core/utils/logger"; +import storeContainerData from "~/core/docker/store-container-stats"; import storeHostData from "~/core/docker/store-host-stats"; +import { logger } from "~/core/utils/logger"; +import type { config } from "~/typings/database"; function convertFromMinToMs(minutes: number): number { - return minutes * 60 * 1000; + return minutes * 60 * 1000; } async function initialRun( - scheduleName: string, - scheduleFunction: Promise | void, - isAsync: boolean + scheduleName: string, + scheduleFunction: Promise | void, + isAsync: boolean, ) { - try { - if (isAsync) { - await scheduleFunction; - } else { - scheduleFunction; - } - logger.info(`Startup run success for: ${scheduleName}`); - } catch (error) { - logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); - } + try { + if (isAsync) { + await scheduleFunction; + } else { + scheduleFunction; + } + logger.info(`Startup run success for: ${scheduleName}`); + } catch (error) { + logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); + } } async function setSchedules() { - try { - const rawConfigData: unknown[] = dbFunctions.getConfig(); - const configData = rawConfigData[0]; + try { + const rawConfigData: unknown[] = dbFunctions.getConfig(); + const configData = rawConfigData[0]; - if ( - !configData || - typeof (configData as config).keep_data_for !== "number" || - typeof (configData as config).fetching_interval !== "number" - ) { - logger.error("Invalid configuration data:", configData); - throw new Error("Invalid configuration data"); - } + if ( + !configData || + typeof (configData as config).keep_data_for !== "number" || + typeof (configData as config).fetching_interval !== "number" + ) { + logger.error("Invalid configuration data:", configData); + throw new Error("Invalid configuration data"); + } - const { keep_data_for, fetching_interval } = configData as config; + const { keep_data_for, fetching_interval } = configData as config; - if (keep_data_for === undefined) { - const errMsg = "keep_data_for is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } + if (keep_data_for === undefined) { + const errMsg = "keep_data_for is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } - if (fetching_interval === undefined) { - const errMsg = "fetching_interval is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } + if (fetching_interval === undefined) { + const errMsg = "fetching_interval is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } - logger.info( - `Scheduling: Fetching container statistics every ${fetching_interval} minutes` - ); + logger.info( + `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, + ); - logger.info( - `Scheduling: Updating host statistics every ${fetching_interval} minutes` - ); + logger.info( + `Scheduling: Updating host statistics every ${fetching_interval} minutes`, + ); - logger.info( - `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days` - ); + logger.info( + `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, + ); - // Schedule container data fetching - await initialRun("storeContainerData", storeContainerData(), true); - setInterval(async () => { - try { - logger.info("Task Start: Fetching container data."); - await storeContainerData(); - logger.info("Task End: Container data fetched successfully."); - } catch (error) { - logger.error("Error in fetching container data:", error); - } - }, convertFromMinToMs(fetching_interval)); + // Schedule container data fetching + await initialRun("storeContainerData", storeContainerData(), true); + setInterval(async () => { + try { + logger.info("Task Start: Fetching container data."); + await storeContainerData(); + logger.info("Task End: Container data fetched successfully."); + } catch (error) { + logger.error("Error in fetching container data:", error); + } + }, convertFromMinToMs(fetching_interval)); - // Schedule Host statistics updates - await initialRun("storeHostData", storeHostData(), true); - setInterval(async () => { - try { - logger.info("Task Start: Updating host stats."); - await storeHostData(); - logger.info("Task End: Updating host stats successfully."); - } catch (error) { - logger.error("Error in updating host stats:", error); - } - }, convertFromMinToMs(fetching_interval)); + // Schedule Host statistics updates + await initialRun("storeHostData", storeHostData(), true); + setInterval(async () => { + try { + logger.info("Task Start: Updating host stats."); + await storeHostData(); + logger.info("Task End: Updating host stats successfully."); + } catch (error) { + logger.error("Error in updating host stats:", error); + } + }, convertFromMinToMs(fetching_interval)); - // Schedule database cleanup - await initialRun( - "dbFunctions.deleteOldData", - dbFunctions.deleteOldData(keep_data_for), - false - ); - setInterval(() => { - try { - logger.info("Task Start: Cleaning up old database data."); - dbFunctions.deleteOldData(keep_data_for); - logger.info("Task End: Database cleanup completed."); - } catch (error) { - logger.error("Error in database cleanup task:", error); - } - }, convertFromMinToMs(60)); + // Schedule database cleanup + await initialRun( + "dbFunctions.deleteOldData", + dbFunctions.deleteOldData(keep_data_for), + false, + ); + setInterval(() => { + try { + logger.info("Task Start: Cleaning up old database data."); + dbFunctions.deleteOldData(keep_data_for); + logger.info("Task End: Database cleanup completed."); + } catch (error) { + logger.error("Error in database cleanup task:", error); + } + }, convertFromMinToMs(60)); - logger.info("Schedules have been set successfully."); - } catch (error) { - logger.error("Error setting schedules:", error); - throw error; - } + logger.info("Schedules have been set successfully."); + } catch (error) { + logger.error("Error setting schedules:", error); + throw error; + } } export { setSchedules }; diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts index 69c12e03..97e0bd99 100644 --- a/src/core/docker/store-container-stats.ts +++ b/src/core/docker/store-container-stats.ts @@ -1,98 +1,98 @@ -import { getDockerClient } from "~/core/docker/client"; +import type Docker from "dockerode"; import { dbFunctions } from "~/core/database"; -import Docker from "dockerode"; +import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { logger } from "../utils/logger"; async function storeContainerData() { - try { - const hosts = dbFunctions.getDockerHosts(); - logger.debug("Retrieved docker hosts for storring container data"); + try { + const hosts = dbFunctions.getDockerHosts(); + logger.debug("Retrieved docker hosts for storring container data"); - // Process each host concurrently and wait for them all to finish - await Promise.all( - hosts.map(async (host) => { - const docker = getDockerClient(host); + // Process each host concurrently and wait for them all to finish + await Promise.all( + hosts.map(async (host) => { + const docker = getDockerClient(host); - // Test the connection with a ping - try { - await docker.ping(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to ping docker host "${host.name}": ${errMsg}`, - ); - } + // Test the connection with a ping + try { + await docker.ping(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to ping docker host "${host.name}": ${errMsg}`, + ); + } - let containers; - try { - containers = await docker.listContainers({ all: true }); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to list containers on host "${host.name}": ${errMsg}`, - ); - } + let containers: Docker.ContainerInfo[] = []; + try { + containers = await docker.listContainers({ all: true }); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to list containers on host "${host.name}": ${errMsg}`, + ); + } - // Process each container concurrently - await Promise.all( - containers.map(async (containerInfo) => { - const containerName = containerInfo.Names[0].replace(/^\//, ""); - try { - const container = docker.getContainer(containerInfo.Id); + // Process each container concurrently + await Promise.all( + containers.map(async (containerInfo) => { + const containerName = containerInfo.Names[0].replace(/^\//, ""); + try { + const container = docker.getContainer(containerInfo.Id); - const stats: Docker.ContainerStats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - return reject( - new Error( - `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, - ), - ); - } - if (!stats) { - return reject( - new Error( - `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".`, - ), - ); - } - resolve(stats); - }); - }, - ); + const stats: Docker.ContainerStats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + return reject( + new Error( + `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, + ), + ); + } + if (!stats) { + return reject( + new Error( + `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".`, + ), + ); + } + resolve(stats); + }); + }, + ); - dbFunctions.addContainerStats( - containerInfo.Id, - host.name, - containerName, - containerInfo.Image, - containerInfo.Status, - containerInfo.State, - calculateCpuPercent(stats), - calculateMemoryUsage(stats), - ); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - throw new Error( - `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, - ); - } - }), - ); - }), - ); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to store container data: ${errMsg}`); - } + dbFunctions.addContainerStats( + containerInfo.Id, + host.name, + containerName, + containerInfo.Image, + containerInfo.Status, + containerInfo.State, + calculateCpuPercent(stats), + calculateMemoryUsage(stats), + ); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error( + `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, + ); + } + }), + ); + }), + ); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to store container data: ${errMsg}`); + } } export default storeContainerData; diff --git a/src/core/docker/store-host-stats.ts b/src/core/docker/store-host-stats.ts index fa7b05d6..053f37ea 100644 --- a/src/core/docker/store-host-stats.ts +++ b/src/core/docker/store-host-stats.ts @@ -1,84 +1,84 @@ -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; -import { DockerHost, HostStats } from "~/typings/docker"; import { getDockerClient } from "~/core/docker/client"; -import { DockerInfo } from "~/typings/dockerode"; import { findObjectByKey } from "~/core/utils/helpers"; +import { logger } from "~/core/utils/logger"; +import type { DockerHost, HostStats } from "~/typings/docker"; +import type { DockerInfo } from "~/typings/dockerode"; function getHostByName(hostName: string): DockerHost { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const foundHost = findObjectByKey(hosts, "name", hostName); - if (!foundHost) { - throw new Error(`Host ${hostName} not found`); - } - return foundHost; + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const foundHost = findObjectByKey(hosts, "name", hostName); + if (!foundHost) { + throw new Error(`Host ${hostName} not found`); + } + return foundHost; } async function storeHostData() { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - await Promise.all( - hosts.map(async (host) => { - const docker = getDockerClient(host); + await Promise.all( + hosts.map(async (host) => { + const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to ping docker host "${host.name}": ${errMsg}`, - ); - } + try { + await docker.ping(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to ping docker host "${host.name}": ${errMsg}`, + ); + } - let hostStats: DockerInfo; - try { - hostStats = await docker.info(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to fetch stats for host "${host.name}": ${errMsg}`, - ); - } + let hostStats: DockerInfo; + try { + hostStats = await docker.info(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to fetch stats for host "${host.name}": ${errMsg}`, + ); + } - const hostId = getHostByName(host.name).id; + const hostId = getHostByName(host.name).id; - if (!hostId) { - throw new Error(`Host "${host.name}" not found`); - } + if (!hostId) { + throw new Error(`Host "${host.name}" not found`); + } - try { - const stats: HostStats = { - hostId: hostId, - hostName: host.name, - dockerVersion: hostStats.ServerVersion, - apiVersion: hostStats.Driver, - os: hostStats.OperatingSystem, - architecture: hostStats.Architecture, - totalMemory: hostStats.MemTotal, - totalCPU: hostStats.NCPU, - labels: hostStats.Labels, - images: hostStats.Images, - containers: hostStats.Containers, - containersPaused: hostStats.ContainersPaused, - containersRunning: hostStats.ContainersRunning, - containersStopped: hostStats.ContainersStopped, - }; + try { + const stats: HostStats = { + hostId: hostId, + hostName: host.name, + dockerVersion: hostStats.ServerVersion, + apiVersion: hostStats.Driver, + os: hostStats.OperatingSystem, + architecture: hostStats.Architecture, + totalMemory: hostStats.MemTotal, + totalCPU: hostStats.NCPU, + labels: hostStats.Labels, + images: hostStats.Images, + containers: hostStats.Containers, + containersPaused: hostStats.ContainersPaused, + containersRunning: hostStats.ContainersRunning, + containersStopped: hostStats.ContainersStopped, + }; - dbFunctions.updateHostStats(stats); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to store stats for host "${host.name}": ${errMsg}`, - ); - } - }), - ); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - logger.error(`storeHostData failed: ${errMsg}`); - throw new Error(`Failed to store host data: ${errMsg}`); - } + dbFunctions.updateHostStats(stats); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to store stats for host "${host.name}": ${errMsg}`, + ); + } + }), + ); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + logger.error(`storeHostData failed: ${errMsg}`); + throw new Error(`Failed to store host data: ${errMsg}`); + } } export default storeHostData; diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts index 6fad398c..854bc5ac 100644 --- a/src/core/plugins/loader.ts +++ b/src/core/plugins/loader.ts @@ -1,53 +1,53 @@ -import { pluginManager } from "./plugin-manager"; -import path from "path"; -import fs from "fs"; -import { logger } from "../utils/logger"; +import fs from "node:fs"; +import path from "node:path"; import { checkFileForChangeMe } from "../utils/change-me-checker"; +import { logger } from "../utils/logger"; +import { pluginManager } from "./plugin-manager"; export async function loadPlugins(pluginDir: string) { - const pluginPath = path.join(process.cwd(), pluginDir); - - logger.debug(`Loading plugins (${pluginPath})`); - - if (!fs.existsSync(pluginPath)) { - throw new Error(`Failed to check plugin directory`); - } - logger.debug(`Plugin directory exists`); - - let pluginCount = 0; - let files; - try { - files = fs.readdirSync(pluginPath); - logger.debug(`Found ${files.length} files in plugin directory`); - } catch (error) { - throw new Error(`Failed to read plugin-directory: ${error}`); - } - - if (!files) { - logger.info(`No plugins found in ${pluginPath}`); - return; - } - - for (const file of files) { - if (!file.endsWith(".plugin.ts")) { - logger.debug(`Skipping non-plugin file: ${file}`); - continue; - } - - const absolutePath = path.join(pluginPath, file); - logger.info(`Loading plugin: ${absolutePath}`); - try { - await checkFileForChangeMe(absolutePath); - const module = await import(absolutePath); - const plugin = module.default; - pluginManager.register(plugin); - pluginCount++; - } catch (error) { - logger.error( - `Error while importing plugin ${absolutePath}: ${error as string}`, - ); - } - } - - logger.info(`Registered ${pluginCount} plugin(s)`); + const pluginPath = path.join(process.cwd(), pluginDir); + + logger.debug(`Loading plugins (${pluginPath})`); + + if (!fs.existsSync(pluginPath)) { + throw new Error("Failed to check plugin directory"); + } + logger.debug("Plugin directory exists"); + + let pluginCount = 0; + let files: string[]; + try { + files = fs.readdirSync(pluginPath); + logger.debug(`Found ${files.length} files in plugin directory`); + } catch (error) { + throw new Error(`Failed to read plugin-directory: ${error}`); + } + + if (!files) { + logger.info(`No plugins found in ${pluginPath}`); + return; + } + + for (const file of files) { + if (!file.endsWith(".plugin.ts")) { + logger.debug(`Skipping non-plugin file: ${file}`); + continue; + } + + const absolutePath = path.join(pluginPath, file); + logger.info(`Loading plugin: ${absolutePath}`); + try { + await checkFileForChangeMe(absolutePath); + const module = await import(absolutePath); + const plugin = module.default; + pluginManager.register(plugin); + pluginCount++; + } catch (error) { + logger.error( + `Error while importing plugin ${absolutePath}: ${error as string}`, + ); + } + } + + logger.info(`Registered ${pluginCount} plugin(s)`); } diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index 55453d0a..83d623f9 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -1,120 +1,120 @@ -import { EventEmitter } from "events"; -import { logger } from "../utils/logger"; -import type { Plugin } from "~/typings/plugin"; +import { EventEmitter } from "node:events"; import type { ContainerInfo } from "~/typings/docker"; +import type { Plugin } from "~/typings/plugin"; +import { logger } from "../utils/logger"; class PluginManager extends EventEmitter { - private plugins: Map = new Map(); - - register(plugin: Plugin) { - try { - this.plugins.set(plugin.name, plugin); - logger.debug(`Registered plugin: ${plugin.name}`); - } catch (error) { - logger.error( - `Registering plugin ${plugin.name} failed: ${error as string}`, - ); - } - } - - unregister(name: string) { - this.plugins.delete(name); - } - - getLoadedPlugins(): string[] { - return Array.from(this.plugins.keys()); - } - - // Trigger plugin flows: - handleContainerStop(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerStop?.(containerInfo); - }); - } - - handleContainerStart(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerStart?.(containerInfo); - }); - } - - handleContainerExit(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerExit?.(containerInfo); - }); - } - - handleContainerCreate(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerCreate?.(containerInfo); - }); - } - - handleContainerDestroy(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerDestroy?.(containerInfo); - }); - } - - handleContainerPause(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerPause?.(containerInfo); - }); - } - - handleContainerUnpause(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerUnpause?.(containerInfo); - }); - } - - handleContainerRestart(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerRestart?.(containerInfo); - }); - } - - handleContainerUpdate(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerUpdate?.(containerInfo); - }); - } - - handleContainerRename(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerRename?.(containerInfo); - }); - } - - handleContainerHealthStatus(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerHealthStatus?.(containerInfo); - }); - } - - handleHostUnreachable(host: string, err: string) { - this.plugins.forEach((plugin) => { - plugin.onHostUnreachable?.(host, err); - }); - } - - handleHostReachableAgain(host: string) { - this.plugins.forEach((plugin) => { - plugin.onHostReachableAgain?.(host); - }); - } - - handleContainerKill(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.onContainerKill?.(containerInfo); - }); - } - - handleContainerDie(containerInfo: ContainerInfo) { - this.plugins.forEach((plugin) => { - plugin.handleContainerDie?.(containerInfo); - }); - } + private plugins: Map = new Map(); + + register(plugin: Plugin) { + try { + this.plugins.set(plugin.name, plugin); + logger.debug(`Registered plugin: ${plugin.name}`); + } catch (error) { + logger.error( + `Registering plugin ${plugin.name} failed: ${error as string}`, + ); + } + } + + unregister(name: string) { + this.plugins.delete(name); + } + + getLoadedPlugins(): string[] { + return Array.from(this.plugins.keys()); + } + + // Trigger plugin flows: + handleContainerStop(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStop?.(containerInfo); + } + } + + handleContainerStart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStart?.(containerInfo); + } + } + + handleContainerExit(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerExit?.(containerInfo); + } + } + + handleContainerCreate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerCreate?.(containerInfo); + } + } + + handleContainerDestroy(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerDestroy?.(containerInfo); + } + } + + handleContainerPause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerPause?.(containerInfo); + } + } + + handleContainerUnpause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUnpause?.(containerInfo); + } + } + + handleContainerRestart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRestart?.(containerInfo); + } + } + + handleContainerUpdate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUpdate?.(containerInfo); + } + } + + handleContainerRename(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRename?.(containerInfo); + } + } + + handleContainerHealthStatus(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerHealthStatus?.(containerInfo); + } + } + + handleHostUnreachable(host: string, err: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostUnreachable?.(host, err); + } + } + + handleHostReachableAgain(host: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostReachableAgain?.(host); + } + } + + handleContainerKill(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerKill?.(containerInfo); + } + } + + handleContainerDie(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.handleContainerDie?.(containerInfo); + } + } } export const pluginManager = new PluginManager(); diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 19bd0824..b6f6ddde 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -1,330 +1,347 @@ -import { dbFunctions } from "~/core/database"; +import { rm } from "node:fs/promises"; +import DockerCompose from "docker-compose"; import YAML from "yaml"; +import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; -import DockerCompose from "docker-compose"; -import type { Stack, ComposeSpec } from "~/typings/docker-compose"; +import { postToClient } from "~/routes/live-stacks"; import type { stacks_config } from "~/typings/database"; -import { rm } from "node:fs/promises"; +import type { ComposeSpec, Stack } from "~/typings/docker-compose"; import { findObjectByKey } from "../utils/helpers"; -import { postToClient } from "~/routes/live-stacks"; const wrapProgressCallback = (progressCallback?: (log: string) => void) => { - return progressCallback - ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { - const log = chunk.toString(); - progressCallback(log); - } - : undefined; + return progressCallback + ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; }; async function getStackName(stack_id: number): Promise { - logger.debug(`Fetching stack name for id ${stack_id}`); - const stacks = dbFunctions.getStacks(); - const stack = findObjectByKey(stacks, "id", stack_id); - if (!stack) { - throw new Error(`Stack with id ${stack_id} not found`); - } - return stack.name; + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = findObjectByKey(stacks, "id", stack_id); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; } async function runStackCommand( - stack_id: number, - command: ( - cwd: string, - progressCallback?: (log: string) => void - ) => Promise, - action: string + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void, + ) => Promise, + action: string, ): Promise { - try { - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); + try { + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); - const progressCallback = (log: string) => { - postToClient({ - type: "stack-progress", - data: { - stack_id, - action, - message: log.trim(), - timestamp: new Date().toISOString(), - }, - }); - }; + const progressCallback = (log: string) => { + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message: log.trim(), + timestamp: new Date().toISOString(), + }, + }); + }; - return await command(stackPath, progressCallback); - } catch (error: any) { - postToClient({ - type: "stack-error", - data: { - stack_id, - action, - message: error.message || String(error), - timestamp: new Date().toISOString(), - }, - }); - throw new Error( - `Error while ${action} stack "${stack_id}": ${error.message || error}` - ); - } + return await command(stackPath, progressCallback); + } catch (error) { + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: String(error), + timestamp: new Date().toISOString(), + }, + }); + throw new Error( + `Error while ${action} stack "${stack_id}": ${String(error)}`, + ); + } } async function getStackPath(stack: Stack): Promise { - const stackName = stack.name.trim().replace(/\s+/g, "_"); - return `stacks/${stackName}`; + const stackName = stack.name.trim().replace(/\s+/g, "_"); + return `stacks/${stackName}`; } async function createStackYAML(compose_spec: Stack): Promise { - const yaml = YAML.stringify(compose_spec.compose_spec); - const stackPath = await getStackPath(compose_spec); - await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { - createPath: true, - }); + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); } export async function deployStack( - stack: ComposeSpec, - name: string, - version: number, - source: string, - automatic_reboot_on_error: boolean, - isCustom: boolean, - image_updates: boolean, - stack_prefix?: string + stack: ComposeSpec, + name: string, + version: number, + source: string, + automatic_reboot_on_error: boolean, + isCustom: boolean, + image_updates: boolean, + stack_prefix?: string, ): Promise { - let stackId: number; + let stackId: number; - try { - logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`); - const serviceCount = stack.services - ? Object.keys(stack.services).length - : 0; - const resolvedPrefix = stack_prefix ?? ""; + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`); + const serviceCount = stack.services + ? Object.keys(stack.services).length + : 0; + const resolvedPrefix = stack_prefix ?? ""; - const stack_config: stacks_config = { - id: 0, - name, - version, - source, - stack_prefix: resolvedPrefix, - automatic_reboot_on_error, - container_count: serviceCount, - custom: isCustom, - image_updates, - }; + const stack_config: stacks_config = { + id: 0, + name, + version, + source, + stack_prefix: resolvedPrefix, + automatic_reboot_on_error, + container_count: serviceCount, + custom: isCustom, + image_updates, + }; - if (!name) { - throw new Error("Stack name needed"); - } + if (!name) { + throw new Error("Stack name needed"); + } - stackId = dbFunctions.addStack(stack_config) as number; - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "pending", - message: "Creating stack configuration", - }, - }); + stackId = dbFunctions.addStack(stack_config) as number; + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }); - const stackYaml: Stack = { - id: stackId, - name, - source, - version, - compose_spec: stack, - }; + const stackYaml: Stack = { + id: stackId, + name, + source, + version, + compose_spec: stack, + }; - await createStackYAML(stackYaml); + await createStackYAML(stackYaml); - await runStackCommand( - stackId, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "deploying" - ); + await runStackCommand( + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying", + ); - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "deployed", - message: "Stack deployed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id: 0, - action: "deploying", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id: 0, + action: "deploying", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } export async function stopStack(stack_id: number): Promise { - // Note the await to discard the result (convert to void) - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.downAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "stopping" - ); + // Note the await to discard the result (convert to void) + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.downAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping", + ); } export async function startStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "starting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting", + ); } export async function pullStackImages(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.pullAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "pulling-images" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images", + ); } export async function restartStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.restartAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "restarting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting", + ); } export async function getStackStatus( - stack_id: number + stack_id: number, + //biome-ignore lint/suspicious/noExplicitAny: ): Promise> { - // Wrap the returned status value to match Promise if that is the expectation. - // In this case, if you need the status, you might adjust the type signature. - const status = await runStackCommand( - stack_id, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check" - ); - return status; + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check", + ); + return status; } export async function removeStack(stack_id: number): Promise { - try { - await runStackCommand( - stack_id, - async (cwd, progressCallback) => { - await DockerCompose.down({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }); - }, - "removing" - ); + try { + await runStackCommand( + stack_id, + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }); + }, + "removing", + ); - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); - try { - await rm(stackPath, { recursive: true }); - } catch (error: any) { - if (error.code !== "ENOENT") throw error; - } + try { + await rm(stackPath, { recursive: true }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } - dbFunctions.deleteStack(stack_id); - postToClient({ - type: "stack-removed", - data: { - stack_id, - message: "Stack removed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + dbFunctions.deleteStack(stack_id); + postToClient({ + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } +//biome-ignore lint/suspicious/noExplicitAny: export async function getAllStacksStatus(): Promise> { - try { - const stacks = dbFunctions.getStacks(); + try { + const stacks = dbFunctions.getStacks(); - const statusResults = await Promise.all( - stacks.map(async (stack) => { - const status = await runStackCommand( - stack.id as number, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check" - ); - return { stackId: stack.id, status }; - }) - ); + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.id as number, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check", + ); + return { stackId: stack.id, status }; + }), + ); - return statusResults.reduce((acc, { stackId, status }) => { - // Ensure stackId is used as a string if necessary, e.g. - acc[String(stackId)] = status; - return acc; - }, {} as Record); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + return statusResults.reduce( + (acc, { stackId, status }) => { + // Ensure stackId is used as a string if necessary, e.g. + acc[String(stackId)] = status; + return acc; + }, + //biome-ignore lint/suspicious/noExplicitAny: + {} as Record, + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index 60ab40b5..1b5c8930 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -1,45 +1,37 @@ import type Docker from "dockerode"; const calculateCpuPercent = (stats: Docker.ContainerStats): number => { - if (stats == null) { - return 0.0; - } + const cpuDelta = + stats.cpu_stats.cpu_usage.total_usage - + stats.precpu_stats.cpu_usage.total_usage; + const systemDelta = + stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; - const cpuDelta = - stats.cpu_stats.cpu_usage.total_usage - - stats.precpu_stats.cpu_usage.total_usage; - const systemDelta = - stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + if (cpuDelta <= 0) { + return 0.0000001; + } - if (cpuDelta <= 0) { - return 0; - } + if (systemDelta <= 0) { + return 0.0000001; + } - if (systemDelta <= 0) { - return 0; - } + const data = (cpuDelta / systemDelta) * 100; - const data = (cpuDelta / systemDelta) * 100; + if (data === null) { + return 0.0000001; + } - if (data === null) { - return 0; - } - - return data; + return data; }; const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { - if (stats == null) { - return 0; - } - - const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; + if (stats.memory_stats.usage === null) { + return 0.0000001; + } - if (data === null) { - return 0; - } + const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; - return data; + return data; }; export { calculateCpuPercent, calculateMemoryUsage }; diff --git a/src/core/utils/change-me-checker.ts b/src/core/utils/change-me-checker.ts index 74860488..d5aefb4b 100644 --- a/src/core/utils/change-me-checker.ts +++ b/src/core/utils/change-me-checker.ts @@ -1,18 +1,18 @@ -import { readFile } from "fs/promises"; +import { readFile } from "node:fs/promises"; import { logger } from "~/core/utils/logger"; export async function checkFileForChangeMe(filePath: string) { - const regex = /change[\W_]*me/i; - let content = ""; - try { - content = await readFile(filePath, "utf-8"); - } catch (error) { - logger.error("Error reading file:", error); - } + const regex = /change[\W_]*me/i; + let content = ""; + try { + content = await readFile(filePath, "utf-8"); + } catch (error) { + logger.error("Error reading file:", error); + } - if (regex.test(content)) { - throw new Error( - `The file contains ${regex.exec(content)}. Please update it.` - ); - } + if (regex.test(content)) { + throw new Error( + `The file contains ${regex.exec(content)}. Please update it.`, + ); + } } diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts index 689e5667..ab13dd40 100644 --- a/src/core/utils/helpers.ts +++ b/src/core/utils/helpers.ts @@ -1,11 +1,11 @@ import { logger } from "./logger"; export function findObjectByKey( - array: T[], - key: keyof T, - value: T[keyof T] + array: T[], + key: keyof T, + value: T[keyof T], ): T | undefined { - logger.debug(`Searching ${String(key)}`); - const data = array.find((item) => item[key] === value); - return data; + logger.debug(`Searching ${String(key)}`); + const data = array.find((item) => item[key] === value); + return data; } diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index a10dfd23..53208767 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -1,174 +1,180 @@ -import path from "path"; -import wrapAnsi from "wrap-ansi"; -import chalk, { ChalkInstance } from "chalk"; +import path from "node:path"; +import chalk, { type ChalkInstance } from "chalk"; import type { TransformableInfo } from "logform"; import { createLogger, format, transports } from "winston"; +import wrapAnsi from "wrap-ansi"; import { dbFunctions } from "~/core/database"; import { logToClients } from "~/routes/live-logs"; -import { log_message } from "~/typings/database"; +import type { log_message } from "~/typings/database"; + +import { backupInProgress } from "../database/_dbState"; const padNewlines = process.env.PAD_NEW_LINES !== "false"; type LogLevel = - | "error" - | "warn" - | "info" - | "debug" - | "verbose" - | "silly" - | "task" - | "ut"; - + | "error" + | "warn" + | "info" + | "debug" + | "verbose" + | "silly" + | "task" + | "ut"; + +// biome-ignore lint/suspicious/noControlCharactersInRegex: const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; const formatTerminalMessage = (message: string, prefix: string): string => { - try { - const cleanPrefix = prefix.replace(ansiRegex, ""); - const maxWidth = process.stdout.columns || 80; - const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); - - if (!padNewlines) return message; - - const wrapped = wrapAnsi(message, wrapWidth, { - trim: true, - hard: true, - wordWrap: true, - }); - - return wrapped - .split("\n") - .map((line, index) => { - return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; - }) - .join("\n"); - } catch (error) { - console.error("Error formatting terminal message:", error); - return message; - } + try { + const cleanPrefix = prefix.replace(ansiRegex, ""); + const maxWidth = process.stdout.columns || 80; + const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); + + if (!padNewlines) return message; + + const wrapped = wrapAnsi(message, wrapWidth, { + trim: true, + hard: true, + wordWrap: true, + }); + + return wrapped + .split("\n") + .map((line, index) => { + return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; + }) + .join("\n"); + } catch (error) { + console.error("Error formatting terminal message:", error); + return message; + } }; const levelColors: Record = { - error: chalk.red.bold, - warn: chalk.yellow.bold, - info: chalk.green.bold, - debug: chalk.blue.bold, - verbose: chalk.cyan.bold, - silly: chalk.magenta.bold, - task: chalk.cyan.bold, - ut: chalk.hex("#9D00FF"), + error: chalk.red.bold, + warn: chalk.yellow.bold, + info: chalk.green.bold, + debug: chalk.blue.bold, + verbose: chalk.cyan.bold, + silly: chalk.magenta.bold, + task: chalk.cyan.bold, + ut: chalk.hex("#9D00FF"), }; const handleWebSocketLog = (log: log_message) => { - try { - logToClients(log); - } catch (error) { - console.error( - `WebSocket logging failed: ${ - error instanceof Error ? error.message : error - }` - ); - } + try { + logToClients(log); + } catch (error) { + console.error( + `WebSocket logging failed: ${ + error instanceof Error ? error.message : error + }`, + ); + } }; const handleDatabaseLog = (log: log_message): void => { - try { - dbFunctions.addLogEntry(log); - } catch (error) { - console.error( - `Database logging failed: ${ - error instanceof Error ? error.message : error - }` - ); - } + if (backupInProgress) { + return; + } + try { + dbFunctions.addLogEntry(log); + } catch (error) { + console.error( + `Database logging failed: ${ + error instanceof Error ? error.message : error + }`, + ); + } }; export const logger = createLogger({ - level: process.env.LOG_LEVEL || "debug", - format: format.combine( - format.timestamp({ format: "DD/MM HH:mm:ss" }), - format((info) => { - const stack = new Error().stack?.split("\n"); - let file = "unknown"; - let line = 0; - - if (stack) { - for (let i = 2; i < stack.length; i++) { - const lineStr = stack[i].trim(); - if ( - !lineStr.includes("node_modules") && - !lineStr.includes(path.basename(__filename)) - ) { - const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); - if (matches) { - file = path.basename(matches[1]); - line = parseInt(matches[2], 10); - break; - } - } - } - } - return { ...info, file, line }; - })(), - format.printf((info) => { - const { timestamp, level, message, file, line } = - info as TransformableInfo & log_message; - let processedLevel = level as LogLevel; - let processedMessage = String(message); - - if (processedMessage.startsWith("__task__")) { - processedMessage = processedMessage - .replace(/__task__/g, "") - .trimStart(); - processedLevel = "task"; - if (processedMessage.startsWith("__db__")) { - processedMessage = processedMessage - .replace(/__db__/g, "") - .trimStart(); - processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; - } - } else if (processedMessage.startsWith("__UT__")) { - processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); - processedLevel = "ut"; - } - - if (file.endsWith("plugin.ts")) { - processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; - } - - const paddedLevel = processedLevel.toUpperCase().padEnd(5); - const coloredLevel = (levelColors[processedLevel] || chalk.white)( - paddedLevel - ); - const coloredContext = chalk.cyan(`${file}:${line}`); - const coloredTimestamp = chalk.yellow(timestamp); - - const prefix = `${paddedLevel} [ ${timestamp} ] - `; - const combinedContent = `${processedMessage} - ${coloredContext}`; - - const formattedMessage = padNewlines - ? formatTerminalMessage(combinedContent, prefix) - : combinedContent; - - handleDatabaseLog({ - level: processedLevel, - timestamp, - message: processedMessage, - file, - line, - }); - handleWebSocketLog({ - level: processedLevel, - timestamp, - message: processedMessage, - file, - line, - }); - - return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; - }) - ), - transports: [new transports.Console()], + level: process.env.LOG_LEVEL || "debug", + format: format.combine( + format.timestamp({ format: "DD/MM HH:mm:ss" }), + format((info) => { + const stack = new Error().stack?.split("\n"); + let file = "unknown"; + let line = 0; + + if (stack) { + for (let i = 2; i < stack.length; i++) { + const lineStr = stack[i].trim(); + if ( + !lineStr.includes("node_modules") && + !lineStr.includes(path.basename(__filename)) + ) { + const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); + if (matches) { + file = path.basename(matches[1]); + line = Number.parseInt(matches[2], 10); + break; + } + } + } + } + return { ...info, file, line }; + })(), + format.printf((info) => { + const { timestamp, level, message, file, line } = + info as TransformableInfo & log_message; + let processedLevel = level as LogLevel; + let processedMessage = String(message); + + if (processedMessage.startsWith("__task__")) { + processedMessage = processedMessage + .replace(/__task__/g, "") + .trimStart(); + processedLevel = "task"; + if (processedMessage.startsWith("__db__")) { + processedMessage = processedMessage + .replace(/__db__/g, "") + .trimStart(); + processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; + } + } else if (processedMessage.startsWith("__UT__")) { + processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); + processedLevel = "ut"; + } + + if (file.endsWith("plugin.ts")) { + processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; + } + + const paddedLevel = processedLevel.toUpperCase().padEnd(5); + const coloredLevel = (levelColors[processedLevel] || chalk.white)( + paddedLevel, + ); + const coloredContext = chalk.cyan(`${file}:${line}`); + const coloredTimestamp = chalk.yellow(timestamp); + + const prefix = `${paddedLevel} [ ${timestamp} ] - `; + const combinedContent = `${processedMessage} - ${coloredContext}`; + + const formattedMessage = padNewlines + ? formatTerminalMessage(combinedContent, prefix) + : combinedContent; + + handleDatabaseLog({ + level: processedLevel, + timestamp, + message: processedMessage, + file, + line, + }); + handleWebSocketLog({ + level: processedLevel, + timestamp, + message: processedMessage, + file, + line, + }); + + return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; + }), + ), + transports: [new transports.Console()], }); diff --git a/src/core/utils/package-json.ts b/src/core/utils/package-json.ts index 9147d2f1..20958a4c 100644 --- a/src/core/utils/package-json.ts +++ b/src/core/utils/package-json.ts @@ -1,25 +1,25 @@ import packageJson from "~/../package.json"; const { version, description, license, dependencies, devDependencies } = - packageJson; + packageJson; let { contributors } = packageJson; const authorName = packageJson.author.name; const authorEmail = packageJson.author.email; const authorWebsite = packageJson.author.url; -if ((contributors = [])) { - contributors = [":(" as never]; +if (contributors.length === 0) { + contributors = [":(" as never]; } export { - version, - description, - authorName, - authorEmail, - authorWebsite, - license, - contributors, - dependencies, - devDependencies, + version, + description, + authorName, + authorEmail, + authorWebsite, + license, + contributors, + dependencies, + devDependencies, }; diff --git a/src/core/utils/response-handler.ts b/src/core/utils/response-handler.ts index 60a11ea0..8bfe6ec3 100644 --- a/src/core/utils/response-handler.ts +++ b/src/core/utils/response-handler.ts @@ -3,36 +3,41 @@ import { logger } from "~/core/utils/logger"; import type { set } from "~/typings/elysiajs"; export const responseHandler = { - error( - set: set, - error: string, - response_message: string, - error_code?: number - ) { - set.status = error_code || 500; - logger.error(`${response_message} - ${error}`); - return { error: `${response_message}` }; - }, + error( + set: set, + error: string, + response_message: string, + error_code?: number, + ) { + set.status = error_code || 500; + logger.error(`${response_message} - ${error}`); + return { error: `${response_message}` }; + }, - ok(set: set, response_message: string) { - set.status = 200; - logger.debug(response_message); - return { success: true }; - }, + ok(set: set, response_message: string) { + set.status = 200; + logger.debug(response_message); + return { success: true, message: response_message }; + }, - simple_error(set: set, response_message: string, status_code?: number) { - set.status = status_code || 502; - logger.warn(response_message); - return { error: response_message }; - }, + simple_error(set: set, response_message: string, status_code?: number) { + set.status = status_code || 502; + logger.warn(response_message); + return { error: response_message }; + }, - reject(set: set, reject: any, response_message: string, error?: string) { - set.status = 501; - if (error) { - logger.error(`${response_message} - ${error}`); - } else { - logger.error(response_message); - } - return reject(new Error(response_message)); - }, + reject( + set: set, + reject: CallableFunction, + response_message: string, + error?: string, + ) { + set.status = 501; + if (error) { + logger.error(`${response_message} - ${error}`); + } else { + logger.error(response_message); + } + return reject(new Error(response_message)); + }, }; diff --git a/src/index.ts b/src/index.ts index cf5edef3..8090d9a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,168 +1,168 @@ -import { Elysia } from "elysia"; +import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; import { swagger } from "@elysiajs/swagger"; -import { serverTiming } from "@elysiajs/server-timing"; +import { Elysia } from "elysia"; -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; -import { loadPlugins } from "~/core/plugins/loader"; -import { setSchedules } from "~/core/docker/scheduler"; import { monitorDockerEvents } from "~/core/docker/monitor"; -import { swaggerReadme } from "~/core/utils/swagger-readme"; +import { setSchedules } from "~/core/docker/scheduler"; +import { loadPlugins } from "~/core/plugins/loader"; +import { logger } from "~/core/utils/logger"; import { - authorWebsite, - contributors, - license, + authorWebsite, + contributors, + license, } from "~/core/utils/package-json"; +import { swaggerReadme } from "~/core/utils/swagger-readme"; import { validateApiKey } from "~/middleware/auth"; -import { backendLogs } from "~/routes/logs"; -import { utilRoutes } from "~/routes/utils"; -import { liveLogs } from "~/routes/live-logs"; -import { stackRoutes } from "~/routes/stacks"; import { apiConfigRoutes } from "~/routes/api-config"; import { dockerRoutes } from "~/routes/docker-manager"; import { dockerStatsRoutes } from "~/routes/docker-stats"; import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; +import { liveLogs } from "~/routes/live-logs"; +import { backendLogs } from "~/routes/logs"; +import { stackRoutes } from "~/routes/stacks"; +import { utilRoutes } from "~/routes/utils"; import { liveStacks } from "./routes/live-stacks"; -import { config } from "~/typings/database"; +import type { config } from "~/typings/database"; console.log(""); logger.info("Starting DockStatAPI"); export const DockStatAPI = new Elysia() - .use(staticPlugin()) - .use(serverTiming()) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey", - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }) - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; - - if (path === "/health" || path.startsWith("/swagger")) { - logger.info(`Requested unguarded route: ${path}`); - return; - } - - const validation = await validateApiKey(request, set); - - if (validation.error) { - set.status = 400; - set.headers["Content-Type"] = "application/json"; - return { error: validation.error }; - } - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(utilRoutes) - .use(stackRoutes) - .use(utilRoutes) - .use(liveLogs) - .use(liveStacks) - .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) - .onError(({ code, set, path }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - }); + .use(staticPlugin()) + .use(serverTiming()) + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "3.0.0", + description: swaggerReadme, + }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey", + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], + }, + }), + ) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; + + if (path === "/health" || path.startsWith("/swagger")) { + logger.info(`Requested unguarded route: ${path}`); + return; + } + + const validation = await validateApiKey(request, set); + + if (validation.error) { + set.status = 400; + set.headers["Content-Type"] = "application/json"; + return { error: validation.error }; + } + }) + .use(dockerRoutes) + .use(dockerStatsRoutes) + .use(backendLogs) + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .use(utilRoutes) + .use(stackRoutes) + .use(utilRoutes) + .use(liveLogs) + .use(liveStacks) + .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) + .onError(({ code, set, path }) => { + if (code === "NOT_FOUND") { + logger.warn(`Unknown route (${path}), showing error page!`); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); + } + }); async function startServer() { - try { - try { - await loadPlugins("./src/plugins"); - } catch (error) { - throw new Error(`Failed to load plugins: ${error}`); - } - - try { - await setSchedules(); - } catch (error) { - throw new Error(`Failed to set schedules: ${error}`); - } - - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); - - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; - - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" - ); - } - - try { - DockStatAPI.listen(3000, ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger` - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }); - } catch (error) { - logger.error("Failed to start server:", error); - process.exit(1); - } - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } + try { + try { + await loadPlugins("./src/plugins"); + } catch (error) { + throw new Error(`Failed to load plugins: ${error}`); + } + + try { + await setSchedules(); + } catch (error) { + throw new Error(`Failed to set schedules: ${error}`); + } + + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); + + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; + + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", + ); + } + + try { + DockStatAPI.listen(3000, ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + }); + } catch (error) { + logger.error("Failed to start server:", error); + process.exit(1); + } + } catch (error) { + logger.error("Error while starting server:", error); + process.exit(1); + } } await startServer(); diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 48aad394..00077932 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -1,82 +1,84 @@ -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; +import { logger } from "~/core/utils/logger"; -import { set } from "~/typings/elysiajs"; -import { config } from "~/typings/database"; +import type { config } from "~/typings/database"; +import type { set } from "~/typings/elysiajs"; export async function hashApiKey(apiKey: string): Promise { - logger.debug("Hashing API key"); - try { - logger.debug("API key hashed successfully"); - return await Bun.password.hash(apiKey); - } catch (error) { - logger.error("Error hashing API key", error); - throw new Error("Failed to hash API key"); - } + logger.debug("Hashing API key"); + try { + logger.debug("API key hashed successfully"); + return await Bun.password.hash(apiKey); + } catch (error) { + logger.error("Error hashing API key", error); + throw new Error("Failed to hash API key"); + } } async function validateApiKeyHash( - providedKey: string, - storedHash: string + providedKey: string, + storedHash: string, ): Promise { - logger.debug("Validating API key hash"); - try { - const isValid = await Bun.password.verify(providedKey, storedHash); - logger.debug(`API key validation result: ${isValid}`); - return isValid; - } catch (error) { - logger.error("Error validating API key hash", error); - return false; - } + logger.debug("Validating API key hash"); + try { + const isValid = await Bun.password.verify(providedKey, storedHash); + logger.debug(`API key validation result: ${isValid}`); + return isValid; + } catch (error) { + logger.error("Error validating API key hash", error); + return false; + } } async function getApiKeyFromDb( - apiKey: string + apiKey: string, ): Promise<{ hash: string } | null> { - const dbApiKey = (dbFunctions.getConfig() as config[])[0].api_key; - logger.debug(`Querying database for API key: ${apiKey}`); - return Promise.resolve({ - hash: dbApiKey, - }); + const dbApiKey = (dbFunctions.getConfig() as config[])[0].api_key; + logger.debug(`Querying database for API key: ${apiKey}`); + return Promise.resolve({ + hash: dbApiKey, + }); } export async function validateApiKey(request: Request, set: set) { - const apiKey = request.headers.get("x-api-key"); + const apiKey = request.headers.get("x-api-key"); + + if (process.env.NODE_ENV !== "production") { + logger.warn( + "API Key validation deactivated, since running in development mode", + ); + return { apiKey }; + } - if (process.env.NODE_ENV != "production") { - logger.warn( - "API Key validation deactivated, since running in development mode" - ); - return { apiKey }; - } else if (!apiKey) { - logger.error(`API key missing from request ${request.url}`); - set.status = 401; - return { error: "API key required" }; - } + if (!apiKey) { + logger.error(`API key missing from request ${request.url}`); + set.status = 401; + return { error: "API key required" }; + } - logger.debug(`API key validation initiated`); + logger.debug("API key validation initiated"); - try { - const dbRecord = await getApiKeyFromDb(apiKey); + try { + const dbRecord = await getApiKeyFromDb(apiKey); - if (!dbRecord) { - logger.error("API key not found in database"); - set.status = 401; - return { error: "Invalid API key" }; - } + if (!dbRecord) { + logger.error("API key not found in database"); + set.status = 401; + return { error: "Invalid API key" }; + } - const isValid = await validateApiKeyHash(apiKey, dbRecord.hash); + const isValid = await validateApiKeyHash(apiKey, dbRecord.hash); - if (!isValid) { - logger.error("Invalid API key provided"); - set.status = 401; - return { error: "Invalid API key" }; - } + if (!isValid) { + logger.error("Invalid API key provided"); + set.status = 401; + return { error: "Invalid API key" }; + } - return logger.info(`Valid API key used`); - } catch (error) { - logger.error("Error during API key validation", error); - set.status = 500; - return { error: "Internal server error" }; - } + return logger.info("Valid API key used"); + } catch (error) { + logger.error("Error during API key validation", error); + set.status = 500; + return { error: "Internal server error" }; + } } diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index e9a97750..633eea41 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -1,98 +1,98 @@ import { logger } from "~/core/utils/logger"; -import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo } from "~/typings/docker"; +import type { Plugin } from "~/typings/plugin"; // See https://outline.itsnik.de/s/dockstat/doc/plugin-development-3UBj9gNMKF for more info const ExamplePlugin: Plugin = { - name: "Example Plugin", - - async onContainerStart(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} started on ${containerInfo.hostId}` - ); - }, - - async onContainerStop(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} stopped on ${containerInfo.hostId}` - ); - }, - - async onContainerExit(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} exited on ${containerInfo.hostId}` - ); - }, - - async onContainerCreate(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} created on ${containerInfo.hostId}` - ); - }, - - async onContainerDestroy(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}` - ); - }, - - async onContainerPause(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} pause on ${containerInfo.hostId}` - ); - }, - - async onContainerUnpause(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} resumed on ${containerInfo.hostId}` - ); - }, - - async onContainerRestart(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} restarted on ${containerInfo.hostId}` - ); - }, - - async onContainerUpdate(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} updated on ${containerInfo.hostId}` - ); - }, - - async onContainerRename(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} renamed on ${containerInfo.hostId}` - ); - }, - - async onContainerHealthStatus(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} changed status to ${containerInfo.status}` - ); - }, - - async onHostUnreachable(host: string, err: string) { - logger.info(`Server ${host} unreachable - ${err}`); - }, - - async onHostReachableAgain(host: string) { - logger.info(`Server ${host} reachable`); - }, - - async handleContainerDie(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} died on ${containerInfo.hostId}` - ); - }, - - async onContainerKill(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} killed on ${containerInfo.hostId}` - ); - }, + name: "Example Plugin", + + async onContainerStart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} started on ${containerInfo.hostId}`, + ); + }, + + async onContainerStop(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} stopped on ${containerInfo.hostId}`, + ); + }, + + async onContainerExit(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} exited on ${containerInfo.hostId}`, + ); + }, + + async onContainerCreate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} created on ${containerInfo.hostId}`, + ); + }, + + async onContainerDestroy(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}`, + ); + }, + + async onContainerPause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} pause on ${containerInfo.hostId}`, + ); + }, + + async onContainerUnpause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} resumed on ${containerInfo.hostId}`, + ); + }, + + async onContainerRestart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} restarted on ${containerInfo.hostId}`, + ); + }, + + async onContainerUpdate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} updated on ${containerInfo.hostId}`, + ); + }, + + async onContainerRename(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} renamed on ${containerInfo.hostId}`, + ); + }, + + async onContainerHealthStatus(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} changed status to ${containerInfo.status}`, + ); + }, + + async onHostUnreachable(host: string, err: string) { + logger.info(`Server ${host} unreachable - ${err}`); + }, + + async onHostReachableAgain(host: string) { + logger.info(`Server ${host} reachable`); + }, + + async handleContainerDie(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} died on ${containerInfo.hostId}`, + ); + }, + + async onContainerKill(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} killed on ${containerInfo.hostId}`, + ); + }, } satisfies Plugin; export default ExamplePlugin; diff --git a/src/plugins/telegram.plugin.ts b/src/plugins/telegram.plugin.ts index eaec24e0..0b83d434 100644 --- a/src/plugins/telegram.plugin.ts +++ b/src/plugins/telegram.plugin.ts @@ -1,35 +1,35 @@ import { logger } from "~/core/utils/logger"; -import type { Plugin } from "~/typings/plugin"; import type { ContainerInfo } from "~/typings/docker"; +import type { Plugin } from "~/typings/plugin"; const TELEGRAM_BOT_TOKEN = "CHANGE_ME"; // Replace with your bot token const TELEGRAM_CHAT_ID = "CHANGE_ME"; // Replace with your chat ID const TelegramNotificationPlugin: Plugin = { - name: "Telegram Notification Plugin", - async onContainerStart(containerInfo: ContainerInfo) { - const message = `Container Started: ${containerInfo.name} on ${containerInfo.hostId}`; - try { - const response = await fetch( - `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - chat_id: TELEGRAM_CHAT_ID, - text: message, - }), - } - ); - if (!response.ok) { - logger.error(`HTTP error ${response.status}`); - } - logger.info("Telegram notification sent."); - } catch (error) { - logger.error("Failed to send Telegram notification", error as string); - } - }, + name: "Telegram Notification Plugin", + async onContainerStart(containerInfo: ContainerInfo) { + const message = `Container Started: ${containerInfo.name} on ${containerInfo.hostId}`; + try { + const response = await fetch( + `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + chat_id: TELEGRAM_CHAT_ID, + text: message, + }), + }, + ); + if (!response.ok) { + logger.error(`HTTP error ${response.status}`); + } + logger.info("Telegram notification sent."); + } catch (error) { + logger.error("Failed to send Telegram notification", error as string); + } + }, } satisfies Plugin; export default TelegramNotificationPlugin; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 0b2c4fb2..5be018ca 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -1,135 +1,257 @@ +import { existsSync, readdir, readdirSync, unlinkSync } from "node:fs"; import { Elysia, t } from "elysia"; - -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; import { pluginManager } from "~/core/plugins/plugin-manager"; -import { responseHandler } from "~/core/utils/response-handler"; +import { logger } from "~/core/utils/logger"; import { - version, - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; +import { responseHandler } from "~/core/utils/response-handler"; +import { backupDir } from "~/core/database/backup"; import { hashApiKey } from "~/middleware/auth"; - -import { config } from "~/typings/database"; +import type { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "/", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; - set.headers["Content-Type"] = "application/json"; - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting the DockStatAPI config" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - }, - } - ) - .get( - "/plugins", - ({ set }) => { - try { - return pluginManager.getLoadedPlugins(); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting all registered plugins" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - }, - } - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; - set.headers["Content-Type"] = "application/json"; - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key) - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - return responseHandler.error( - set, - "Error updating the DockStatAPI config", - error as string - ); - } - }, - { - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - }, - } - ) - .get( - "/package", - async ({ set }) => { - try { - logger.debug("Fetching package.json"); - return { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error while reading package.json" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - }, - } - ); + .get( + "/", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; + set.headers["Content-Type"] = "application/json"; + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error getting the DockStatAPI config", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Returns current API configuration including data retention policies and security settings", + }, + }, + ) + .get( + "/plugins", + ({ set }) => { + try { + return pluginManager.getLoadedPlugins(); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error getting all registered plugins", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + }, + }, + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { fetching_interval, keep_data_for, api_key } = body; + set.headers["Content-Type"] = "application/json"; + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key), + ); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + return responseHandler.error( + set, + "Error updating the DockStatAPI config", + error as string, + ); + } + }, + { + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), + detail: { + tags: ["Management"], + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", + }, + }, + ) + .get( + "/package", + async ({ set }) => { + try { + logger.debug("Fetching package.json"); + return { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error while reading package.json", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Displays package metadata including dependencies, contributors, and licensing information", + }, + }, + ) + .post( + "/backup", + async ({ set }) => { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return responseHandler.ok(set, backupFilename); + } catch (error) { + return responseHandler.error(set, error as string, "Error backing up"); + } + }, + { + detail: { + tags: ["Management"], + description: "Backs up the internal database", + }, + }, + ) + .get( + "/backup", + async ({ set }) => { + try { + const backupFiles = readdirSync(backupDir); + + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); + + return filteredFiles; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Reading Backup directory", + ); + } + }, + { + detail: { + tags: ["Management"], + description: "Lists all available backups", + }, + }, + ) + + .get( + "/backup/download", + async ({ query, set }) => { + try { + const filename = query.filename || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; + + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } + + set.headers["Content-Type"] = "application/octet-stream"; + set.headers["Content-Disposition"] = + `attachment; filename="${filename}"`; + return Bun.file(filePath); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Backup download failed", + ); + } + }, + { + query: t.Object({ + filename: t.Optional(t.String()), + }), + detail: { + tags: ["Management"], + description: + "Download a specific backup or the latest if no filename is provided", + }, + }, + ) + .post( + "/restore", + async ({ body, set }) => { + try { + const { file } = body; + + set.headers["Content-Type"] = "text/html"; + + if (!file) { + throw new Error("No file uploaded"); + } + + if (!file.name.endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } + + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return responseHandler.ok(set, "Database restored successfully"); + } catch (error) { + return responseHandler.error( + set, + error instanceof Error ? error.message : "Restoration failed", + "Database restoration error", + ); + } + }, + { + body: t.Object({ file: t.File() }), + detail: { + tags: ["Management"], + description: "Restore database from uploaded backup file", + }, + }, + ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 35747518..8caadd2f 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -1,119 +1,119 @@ import { Elysia, t } from "elysia"; -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; +import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; -import { DockerHost } from "~/typings/docker"; +import type { DockerHost } from "~/typings/docker"; export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) - .post( - "/add-host", - async ({ set, body }) => { - try { - set.headers["Content-Type"] = "application/json"; - dbFunctions.addDockerHost(body as DockerHost); - return responseHandler.ok(set, `Added docker host (${body.name})`); - } catch (error: unknown) { - return responseHandler.error( - set, - "Error adding docker Host", - error as string - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Registers a new Docker host to the monitoring system with connection details", - }, - body: t.Object({ - name: t.String(), - hostAddress: t.String(), - secure: t.Boolean(), - }), - } - ) + .post( + "/add-host", + async ({ set, body }) => { + try { + set.headers["Content-Type"] = "application/json"; + dbFunctions.addDockerHost(body as DockerHost); + return responseHandler.ok(set, `Added docker host (${body.name})`); + } catch (error: unknown) { + return responseHandler.error( + set, + "Error adding docker Host", + error as string, + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Registers a new Docker host to the monitoring system with connection details", + }, + body: t.Object({ + name: t.String(), + hostAddress: t.String(), + secure: t.Boolean(), + }), + }, + ) - .post( - "/update-host", - async ({ set, body }) => { - try { - set.status = 200; - dbFunctions.updateDockerHost(body); - return responseHandler.ok(set, `Updated docker host (${body.id})`); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to update host" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies existing Docker host configuration parameters (name, address, security)", - }, - body: t.Object({ - id: t.Number(), - name: t.String(), - hostAddress: t.String(), - secure: t.Boolean(), - }), - } - ) + .post( + "/update-host", + async ({ set, body }) => { + try { + set.status = 200; + dbFunctions.updateDockerHost(body); + return responseHandler.ok(set, `Updated docker host (${body.id})`); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to update host", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies existing Docker host configuration parameters (name, address, security)", + }, + body: t.Object({ + id: t.Number(), + name: t.String(), + hostAddress: t.String(), + secure: t.Boolean(), + }), + }, + ) - .get( - "/hosts", - async ({ set }) => { - try { - const dockerHosts = dbFunctions.getDockerHosts(); - set.headers["Content-Type"] = "application/json"; - logger.debug("Retrieved docker hosts"); - return dockerHosts; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve hosts" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all configured Docker hosts with their connection settings", - }, - } - ) + .get( + "/hosts", + async ({ set }) => { + try { + const dockerHosts = dbFunctions.getDockerHosts(); + set.headers["Content-Type"] = "application/json"; + logger.debug("Retrieved docker hosts"); + return dockerHosts; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve hosts", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all configured Docker hosts with their connection settings", + }, + }, + ) - .delete( - "/hosts/:id", - async ({ set, params }) => { - try { - set.status = 200; - dbFunctions.deleteDockerHost(params.id); - return responseHandler.ok(set, `Deleted docker host (${params.id})`); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to delete host" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Removes Docker host from monitoring system and clears associated data", - }, - params: t.Object({ - id: t.Number(), - }), - } - ); + .delete( + "/hosts/:id", + async ({ set, params }) => { + try { + set.status = 200; + dbFunctions.deleteDockerHost(params.id); + return responseHandler.ok(set, `Deleted docker host (${params.id})`); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to delete host", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Removes Docker host from monitoring system and clears associated data", + }, + params: t.Object({ + id: t.Number(), + }), + }, + ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 3c31c5c9..d804afaf 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -1,166 +1,166 @@ -import Docker from "dockerode"; +import type Docker from "dockerode"; import { Elysia } from "elysia"; -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; -import { findObjectByKey } from "~/core/utils/helpers"; -import { responseHandler } from "~/core/utils/response-handler"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; +import { findObjectByKey } from "~/core/utils/helpers"; +import { logger } from "~/core/utils/logger"; +import { responseHandler } from "~/core/utils/response-handler"; -import type { DockerInfo } from "~/typings/dockerode"; import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; +import type { DockerInfo } from "~/typings/dockerode"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get( - "/containers", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; + .get( + "/containers", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - return responseHandler.error( - set, - pingError as string, - "Docker host connection failed" - ); - } + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + return responseHandler.error( + set, + pingError as string, + "Docker host connection failed", + ); + } - const hostContainers = await docker.listContainers({ all: true }); + const hostContainers = await docker.listContainers({ all: true }); - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - return responseHandler.reject( - set, - reject, - "An error occurred", - error - ); - } - if (!stats) { - return responseHandler.reject( - set, - reject, - "No stats available" - ); - } - resolve(stats); - }); - } - ); + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return responseHandler.reject( + set, + reject, + "An error occurred", + error, + ); + } + if (!stats) { + return responseHandler.reject( + set, + reject, + "No stats available", + ); + } + resolve(stats); + }); + }, + ); - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError - ); - } - }) - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (hostError) { - logger.error("Error fetching containers for host,", hostError); - } - }) - ); + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (hostError) { + logger.error("Error fetching containers for host,", hostError); + } + }), + ); - set.headers["Content-Type"] = "application/json"; - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve containers" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", - }, - } - ) + set.headers["Content-Type"] = "application/json"; + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve containers", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", + }, + }, + ) - .get( - "/hosts/:id", - async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = findObjectByKey(hosts, "name", params.id); - if (!host) { - return responseHandler.simple_error( - set, - `Host (${params.id}) not found` - ); - } + .get( + "/hosts/:id", + async ({ params, set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const host = findObjectByKey(hosts, "name", params.id); + if (!host) { + return responseHandler.simple_error( + set, + `Host (${params.id}) not found`, + ); + } - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - set.headers["Content-Type"] = "application/json"; - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - }, - } - ); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + }, + }, + ); diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 3165eff9..51aefcd8 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -1,134 +1,136 @@ -import split2 from "split2"; +import type { Readable } from "node:stream"; import { Elysia } from "elysia"; -import type { Readable } from "stream"; import type { ElysiaWS } from "elysia/dist/ws"; +import split2 from "split2"; -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; -import { responseHandler } from "~/core/utils/response-handler"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; +import { logger } from "~/core/utils/logger"; +import { responseHandler } from "~/core/utils/response-handler"; +//biome-ignore lint/suspicious/noExplicitAny: const activeDockerConnections = new Set>(); const connectionStreams = new Map< - ElysiaWS, - Array<{ statsStream: Readable; splitStream: ReturnType }> + //biome-ignore lint/suspicious/noExplicitAny: + ElysiaWS, + Array<{ statsStream: Readable; splitStream: ReturnType }> >(); export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( - "/stats", - { - async open(ws) { - activeDockerConnections.add(ws); - connectionStreams.set(ws, []); + "/stats", + { + async open(ws) { + activeDockerConnections.add(ws); + connectionStreams.set(ws, []); - ws.send(JSON.stringify({ message: "Connection established" })); - logger.info(`New Docker WebSocket established (${ws.id})`); + ws.send(JSON.stringify({ message: "Connection established" })); + logger.info(`New Docker WebSocket established (${ws.id})`); - try { - const hosts = dbFunctions.getDockerHosts(); - logger.debug(`Retrieved ${hosts.length} docker host(s)`); + try { + const hosts = dbFunctions.getDockerHosts(); + logger.debug(`Retrieved ${hosts.length} docker host(s)`); - for (const host of hosts) { - if (ws.readyState !== 1) { - break; - } + for (const host of hosts) { + if (ws.readyState !== 1) { + break; + } - const docker = getDockerClient(host); - await docker.ping(); - const containers = await docker.listContainers({ all: true }); - logger.debug( - `Found ${containers.length} containers on ${host.name} (id: ${host.id})` - ); + const docker = getDockerClient(host); + await docker.ping(); + const containers = await docker.listContainers({ all: true }); + logger.debug( + `Found ${containers.length} containers on ${host.name} (id: ${host.id})`, + ); - for (const containerInfo of containers) { - if (ws.readyState !== 1) { - break; - } + for (const containerInfo of containers) { + if (ws.readyState !== 1) { + break; + } - const container = docker.getContainer(containerInfo.Id); - const statsStream = (await container.stats({ - stream: true, - })) as Readable; - const splitStream = split2(); + const container = docker.getContainer(containerInfo.Id); + const statsStream = (await container.stats({ + stream: true, + })) as Readable; + const splitStream = split2(); - connectionStreams.get(ws)?.push({ statsStream, splitStream }); + connectionStreams.get(ws)?.push({ statsStream, splitStream }); - statsStream - .on("close", () => splitStream.destroy()) - .pipe(splitStream) - .on("data", (line: string) => { - if (ws.readyState !== 1 || !line) { - return; - } - try { - const stats = JSON.parse(line); - ws.send( - JSON.stringify({ - id: containerInfo.Id, - hostId: host.id, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats) || 0, - memoryUsage: calculateMemoryUsage(stats) || 0, - }) - ); - } catch (error) { - logger.error(`Parse error: ${error}`); - } - }) - .on("error", (error: Error) => { - logger.error(`Stream error: ${error}`); - statsStream.destroy(); - ws.send( - JSON.stringify({ - hostId: host.name, - containerId: containerInfo.Id, - error: `Stats stream error: ${error}`, - }) - ); - }); - } - } - } catch (error) { - logger.error(`Connection error: ${error}`); - ws.send( - JSON.stringify( - responseHandler.error( - { headers: {} }, - error as string, - "Docker connection failed", - 500 - ) - ) - ); - } - }, + statsStream + .on("close", () => splitStream.destroy()) + .pipe(splitStream) + .on("data", (line: string) => { + if (ws.readyState !== 1 || !line) { + return; + } + try { + const stats = JSON.parse(line); + ws.send( + JSON.stringify({ + id: containerInfo.Id, + hostId: host.id, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats) || 0, + memoryUsage: calculateMemoryUsage(stats) || 0, + }), + ); + } catch (error) { + logger.error(`Parse error: ${error}`); + } + }) + .on("error", (error: Error) => { + logger.error(`Stream error: ${error}`); + statsStream.destroy(); + ws.send( + JSON.stringify({ + hostId: host.name, + containerId: containerInfo.Id, + error: `Stats stream error: ${error}`, + }), + ); + }); + } + } + } catch (error) { + logger.error(`Connection error: ${error}`); + ws.send( + JSON.stringify( + responseHandler.error( + { headers: {} }, + error as string, + "Docker connection failed", + 500, + ), + ), + ); + } + }, - message(ws, message) { - if (message === "pong") ws.pong(); - }, + message(ws, message) { + if (message === "pong") ws.pong(); + }, - close(ws) { - logger.info(`Closing connection ${ws.id}`); - activeDockerConnections.delete(ws); + close(ws) { + logger.info(`Closing connection ${ws.id}`); + activeDockerConnections.delete(ws); - const streams = connectionStreams.get(ws) || []; - streams.forEach(({ statsStream, splitStream }) => { - try { - statsStream.unpipe(splitStream); - statsStream.destroy(); - splitStream.destroy(); - } catch (error) { - logger.error(`Cleanup error: ${error}`); - } - }); - connectionStreams.delete(ws); - }, - } + const streams = connectionStreams.get(ws) || []; + for (const { statsStream, splitStream } of streams) { + try { + statsStream.unpipe(splitStream); + statsStream.destroy(); + splitStream.destroy(); + } catch (error) { + logger.error(`Cleanup error: ${error}`); + } + } + connectionStreams.delete(ws); + }, + }, ); diff --git a/src/routes/live-logs.ts b/src/routes/live-logs.ts index 17c4c69e..2e894b60 100644 --- a/src/routes/live-logs.ts +++ b/src/routes/live-logs.ts @@ -3,29 +3,30 @@ import type { ElysiaWS } from "elysia/dist/ws"; import { logger } from "~/core/utils/logger"; -import { log_message } from "~/typings/database"; +import type { log_message } from "~/typings/database"; +//biome-ignore lint/suspicious/noExplicitAny: const activeConnections = new Set>(); export const liveLogs = new Elysia({ prefix: "/logs" }).ws("/ws", { - open(ws) { - activeConnections.add(ws); - ws.send({ message: "Connection established" }); - logger.info(`New Logs WebSocket established (${ws.id})`); - }, - close(ws) { - logger.info(`Logs WebSocket closed (${ws.id})`); - activeConnections.delete(ws); - }, + open(ws) { + activeConnections.add(ws); + ws.send({ message: "Connection established" }); + logger.info(`New Logs WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Logs WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, }); export function logToClients(data: log_message) { - activeConnections.forEach((ws) => { - try { - ws.send(JSON.stringify(data)); - } catch (error) { - activeConnections.delete(ws); - logger.error("Failed to send to WebSocket:", error); - } - }); + for (const ws of activeConnections) { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + } } diff --git a/src/routes/live-stacks.ts b/src/routes/live-stacks.ts index 4f3d395c..b3a14e74 100644 --- a/src/routes/live-stacks.ts +++ b/src/routes/live-stacks.ts @@ -2,29 +2,30 @@ import { Elysia } from "elysia"; import type { ElysiaWS } from "elysia/dist/ws"; import { logger } from "~/core/utils/logger"; -import { stackSocketMessage } from "~/typings/websocket"; +import type { stackSocketMessage } from "~/typings/websocket"; +//biome-ignore lint/suspicious/noExplicitAny: Any = Connections const activeConnections = new Set>(); export const liveStacks = new Elysia().ws("/stacks", { - open(ws) { - activeConnections.add(ws); - ws.send({ message: "Connection established" }); - logger.info(`New Stacks WebSocket established (${ws.id})`); - }, - close(ws) { - logger.info(`Stacks WebSocket closed (${ws.id})`); - activeConnections.delete(ws); - }, + open(ws) { + activeConnections.add(ws); + ws.send({ message: "Connection established" }); + logger.info(`New Stacks WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Stacks WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, }); export function postToClient(data: stackSocketMessage) { - activeConnections.forEach((ws) => { - try { - ws.send(JSON.stringify(data)); - } catch (error) { - activeConnections.delete(ws); - logger.error("Failed to send to WebSocket:", error); - } - }); + for (const ws of activeConnections) { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + } } diff --git a/src/routes/logs.ts b/src/routes/logs.ts index ce33235a..626e7230 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -1,95 +1,95 @@ import { Elysia } from "elysia"; -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; +import { logger } from "~/core/utils/logger"; export const backendLogs = new Elysia({ prefix: "/logs" }) - .get( - "/", - async ({ set }) => { - try { - const logs = dbFunctions.getAllLogs(); - set.headers["Content-Type"] = "application/json"; - logger.debug(`Retrieved all logs`); - return logs; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve logs,", error); - return { error: "Failed to retrieve logs" }; - } - }, - { - detail: { - tags: ["Management"], - description: - "Retrieves complete application log history from persistent storage", - }, - } - ) + .get( + "/", + async ({ set }) => { + try { + const logs = dbFunctions.getAllLogs(); + set.headers["Content-Type"] = "application/json"; + logger.debug("Retrieved all logs"); + return logs; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve logs,", error); + return { error: "Failed to retrieve logs" }; + } + }, + { + detail: { + tags: ["Management"], + description: + "Retrieves complete application log history from persistent storage", + }, + }, + ) - .get( - "/:level", - async ({ params: { level }, set }) => { - try { - const logs = dbFunctions.getLogsByLevel(level); - set.headers["Content-Type"] = "application/json"; - logger.debug(`Retrieved logs (level: ${level})`); - return logs; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve logs"); - return { error: "Failed to retrieve logs" }; - } - }, - { - detail: { - tags: ["Management"], - description: - "Filters logs by severity level (debug, info, warn, error, fatal)", - }, - } - ) + .get( + "/:level", + async ({ params: { level }, set }) => { + try { + const logs = dbFunctions.getLogsByLevel(level); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Retrieved logs (level: ${level})`); + return logs; + } catch (error) { + set.status = 500; + logger.error("Failed to retrieve logs"); + return { error: "Failed to retrieve logs" }; + } + }, + { + detail: { + tags: ["Management"], + description: + "Filters logs by severity level (debug, info, warn, error, fatal)", + }, + }, + ) - .delete( - "/", - async ({ set }) => { - try { - set.status = 200; - set.headers["Content-Type"] = "application/json"; - dbFunctions.clearAllLogs(); - return { success: true }; - } catch (error) { - set.status = 500; - logger.error("Could not delete all logs,", error); - return { error: "Could not delete all logs" }; - } - }, - { - detail: { - tags: ["Management"], - description: "Purges all historical log records from the database", - }, - } - ) + .delete( + "/", + async ({ set }) => { + try { + set.status = 200; + set.headers["Content-Type"] = "application/json"; + dbFunctions.clearAllLogs(); + return { success: true }; + } catch (error) { + set.status = 500; + logger.error("Could not delete all logs,", error); + return { error: "Could not delete all logs" }; + } + }, + { + detail: { + tags: ["Management"], + description: "Purges all historical log records from the database", + }, + }, + ) - .delete( - "/:level", - async ({ params: { level }, set }) => { - try { - dbFunctions.clearLogsByLevel(level); - set.headers["Content-Type"] = "application/json"; - logger.debug(`Cleared all logs with level: ${level}`); - return { success: true }; - } catch (error) { - set.status = 500; - logger.error("Could not clear logs with level", level, ",", error); - return { error: "Failed to retrieve logs" }; - } - }, - { - detail: { - tags: ["Management"], - description: "Clears log entries matching specified severity level", - }, - } - ); + .delete( + "/:level", + async ({ params: { level }, set }) => { + try { + dbFunctions.clearLogsByLevel(level); + set.headers["Content-Type"] = "application/json"; + logger.debug(`Cleared all logs with level: ${level}`); + return { success: true }; + } catch (error) { + set.status = 500; + logger.error("Could not clear logs with level", level, ",", error); + return { error: "Failed to retrieve logs" }; + } + }, + { + detail: { + tags: ["Management"], + description: "Clears log entries matching specified severity level", + }, + }, + ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 528a5c1a..56c8d1b6 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -1,302 +1,291 @@ import { Elysia, t } from "elysia"; -import { logger } from "~/core/utils/logger"; import { dbFunctions } from "~/core/database"; -import { responseHandler } from "~/core/utils/response-handler"; import { - deployStack, - stopStack, - pullStackImages, - restartStack, - getStackStatus, - startStack, - getAllStacksStatus, - removeStack, + deployStack, + getAllStacksStatus, + getStackStatus, + pullStackImages, + removeStack, + restartStack, + startStack, + stopStack, } from "~/core/stacks/controller"; +import { logger } from "~/core/utils/logger"; +import { responseHandler } from "~/core/utils/response-handler"; export const stackRoutes = new Elysia({ prefix: "/stacks" }) - .post( - "/deploy", - async ({ set, body }) => { - try { - const isCustom = body.isCustom || false; + .post( + "/deploy", + async ({ set, body }) => { + try { + const isCustom = body.isCustom || false; + + const image_updates = body.image_updates || false; + + const missingParams: string[] = []; + if (!body.compose_spec) { + missingParams.push("compose_spec"); + } + if (body.automatic_reboot_on_error === undefined) { + missingParams.push("automatic_reboot_on_error"); + } + if (!body.source) { + missingParams.push("source"); + } + if (!body.name) { + missingParams.push("name"); + } + + if (missingParams.length > 0) { + const errMsg = `Missing values of: ${missingParams.join("; ")}`; + return responseHandler.error(set, errMsg, errMsg); + } + + await deployStack( + body.compose_spec, + body.name, + body.version, + body.source, + body.automatic_reboot_on_error, + isCustom, + image_updates, + body.stack_prefix, + ); + logger.info(`Deployed Stack (${body.name})`); + return responseHandler.ok( + set, + `Stack ${body.name} deployed successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return responseHandler.error(set, errorMsg, "Error deploying stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Deploys a new Docker stack using a provided compose specification, allowing custom configurations and image updates", + }, + body: t.Object({ + compose_spec: t.Any(), + name: t.String(), + version: t.Number(), + automatic_reboot_on_error: t.Boolean(), + isCustom: t.Boolean(), + image_updates: t.Boolean(), + source: t.String(), + stack_prefix: t.Optional(t.String()), + }), + }, + ) + .post( + "/start", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack ID needed"); + } + await startStack(body.stackId); + logger.info(`Started Stack (${body.stackId})`); + return responseHandler.ok( + set, + `Stack ${body.stackId} started successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return responseHandler.error(set, errorMsg, "Error starting stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Initiates a Docker stack, starting all associated containers", + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ) + .post( + "/stop", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack needed"); + } + await stopStack(body.stackId); + logger.info(`Stopped Stack (${body.stackId})`); + return responseHandler.ok( + set, + `Stack ${body.stackId} stopped successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return responseHandler.error(set, errorMsg, "Error stopping stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Halts a running Docker stack and its containers while preserving configurations", + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ) + .post( + "/restart", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack needed"); + } + await restartStack(body.stackId); + logger.info(`Restarted Stack (${body.stackId})`); + return responseHandler.ok( + set, + `Stack ${body.stackId} restarted successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return responseHandler.error(set, errorMsg, "Error restarting stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Performs full stack restart - stops and restarts all stack components in sequence", + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ) + .post( + "/pull-images", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack needed"); + } + await pullStackImages(body.stackId); + logger.info(`Pulled Stack images (${body.stackId})`); + return responseHandler.ok( + set, + `Images for stack ${body.stackId} pulled successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - const image_updates = body.image_updates || false; + return responseHandler.error(set, errorMsg, "Error pulling images"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Updates container images for a stack using Docker's pull mechanism (requires stack ID)", + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ) + .get( + "/status", + async ({ set, query }) => { + try { + //biome-ignore lint/suspicious/noExplicitAny: + let status: Record; + let res = {}; + if (query.stackId) { + status = await getStackStatus(query.stackId); + res = responseHandler.ok( + set, + `Stack ${query.stackId} status retrieved successfully`, + ); + logger.info("Fetched Stack status"); + } else { + status = await getAllStacksStatus(); + res = responseHandler.ok(set, "Fetched all Stack's status"); + logger.info("Fetched all Stack status"); + } + return { ...res, status: status }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - let missingParams: string[] = []; - if (!body.compose_spec) { - missingParams.push("compose_spec"); - } - if (body.automatic_reboot_on_error === undefined) { - missingParams.push("automatic_reboot_on_error"); - } - if (!body.source) { - missingParams.push("source"); - } - if (!body.name) { - missingParams.push("name"); - } + return responseHandler.error( + set, + errorMsg, + "Error getting stack status", + ); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Retrieves operational status for either a specific stack (by ID) or all managed stacks", + }, + query: t.Object({ + stackId: t.Number(), + }), + }, + ) + .get( + "/", + async ({ set }) => { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks"); + return stacks; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - if (missingParams.length > 0) { - const errMsg = `Missing values of: ${missingParams.join("; ")}`; - return responseHandler.error(set, errMsg, errMsg); - } + return responseHandler.error(set, errorMsg, "Error getting stacks"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Lists all registered stacks with their complete configuration details", + }, + }, + ) - await deployStack( - body.compose_spec, - body.name, - body.version, - body.source, - body.automatic_reboot_on_error, - isCustom, - image_updates, - body.stack_prefix - ); - logger.info(`Deployed Stack (${body.name})`); - return responseHandler.ok( - set, - `Stack ${body.name} deployed successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error deploying stack" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Deploys a new Docker stack using a provided compose specification, allowing custom configurations and image updates", - }, - body: t.Object({ - compose_spec: t.Any(), - name: t.String(), - version: t.Number(), - automatic_reboot_on_error: t.Boolean(), - isCustom: t.Boolean(), - image_updates: t.Boolean(), - source: t.String(), - stack_prefix: t.Optional(t.String()), - }), - } - ) - .post( - "/start", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack ID needed"); - } - await startStack(body.stackId); - logger.info(`Started Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} started successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error starting stack" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Initiates a Docker stack, starting all associated containers", - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ) - .post( - "/stop", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await stopStack(body.stackId); - logger.info(`Stopped Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} stopped successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error stopping stack" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Halts a running Docker stack and its containers while preserving configurations", - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ) - .post( - "/restart", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await restartStack(body.stackId); - logger.info(`Restarted Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} restarted successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error restarting stack" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Performs full stack restart - stops and restarts all stack components in sequence", - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ) - .post( - "/pull-images", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await pullStackImages(body.stackId); - logger.info(`Pulled Stack images (${body.stackId})`); - return responseHandler.ok( - set, - `Images for stack ${body.stackId} pulled successfully` - ); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error pulling images" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Updates container images for a stack using Docker's pull mechanism (requires stack ID)", - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ) - .get( - "/status", - async ({ set, query }) => { - try { - let status; - let res = {}; - if (query.stackId) { - status = await getStackStatus(query.stackId); - res = responseHandler.ok( - set, - `Stack ${query.stackId} status retrieved successfully` - ); - logger.info("Fetched Stack status"); - } else { - status = await getAllStacksStatus(); - res = responseHandler.ok(set, "Fetched all Stack's status"); - logger.info("Fetched all Stack status"); - } - return { ...res, status: status }; - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error getting stack status" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Retrieves operational status for either a specific stack (by ID) or all managed stacks", - }, - query: t.Object({ - stackId: t.Number(), - }), - } - ) - .get( - "/", - async ({ set }) => { - try { - const stacks = dbFunctions.getStacks(); - logger.info("Fetched Stacks"); - return stacks; - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error getting stacks" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Lists all registered stacks with their complete configuration details", - }, - } - ) + .delete( + "/", + async ({ set, body }) => { + try { + const { stackId } = body; + await removeStack(stackId); + logger.info(`Deleted Stack ${stackId}`); + return responseHandler.ok(set, `Stack ${stackId} deleted successfully`); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - .delete( - "/", - async ({ set, body }) => { - try { - const { stackId } = body; - await removeStack(stackId); - logger.info(`Deleted Stack ${stackId}`); - return responseHandler.ok(set, `Stack ${stackId} deleted successfully`); - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error deleting stack" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Permanently removes a stack configuration and cleans up associated resources", - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ); + return responseHandler.error(set, errorMsg, "Error deleting stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Permanently removes a stack configuration and cleans up associated resources", + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ); diff --git a/src/routes/utils.ts b/src/routes/utils.ts index 17cba245..b578f92b 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -1,47 +1,47 @@ import { Elysia, t } from "elysia"; -import { responseHandler } from "~/core/utils/response-handler"; import { - version, - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; +import { responseHandler } from "~/core/utils/response-handler"; export const utilRoutes = new Elysia({ prefix: "/utils" }).get( - "/info", - async ({ set }) => { - try { - set.status = 200; - return { - version, - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - }; - } catch (error: any) { - return responseHandler.error( - set, - error.message || error, - "Error getting DockStatAPI information" - ); - } - }, - { - detail: { - tags: ["Utils"], - description: - "Retrieves DockStatAPI metadata including version, author information, dependencies, and licensing details", - }, - } + "/info", + async ({ set }) => { + try { + set.status = 200; + return { + version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + }; + } catch (error) { + return responseHandler.error( + set, + String(error), + "Error getting DockStatAPI information", + ); + } + }, + { + detail: { + tags: ["Utils"], + description: + "Retrieves DockStatAPI metadata including version, author information, dependencies, and licensing details", + }, + }, ); diff --git a/src/tests/cleanup.ts b/src/tests/cleanup.ts index 31756dda..ac90de71 100644 --- a/src/tests/cleanup.ts +++ b/src/tests/cleanup.ts @@ -6,15 +6,15 @@ import type { DockerHost } from "~/typings/docker"; console.log(""); console.log("Deleting `test` Docker host"); -let testHosts: DockerHost[] = dbFunctions.getDockerHosts(); +const testHosts: DockerHost[] = dbFunctions.getDockerHosts(); const testHost = findObjectByKey(testHosts, "name", "test"); if (testHost) { - dbFunctions.deleteDockerHost(testHost.id as number); - console.log(`Docker host with name "${testHost.name}" deleted.`); + dbFunctions.deleteDockerHost(testHost.id as number); + console.log(`Docker host with name "${testHost.name}" deleted.`); } else { - console.log("Docker host not found."); + console.log("Docker host not found."); } console.log("Cleaning up Database config to default values"); diff --git a/src/tests/delete.spec.ts b/src/tests/delete.spec.ts index 097ede0d..901b05f6 100644 --- a/src/tests/delete.spec.ts +++ b/src/tests/delete.spec.ts @@ -3,11 +3,11 @@ import { describe, it } from "bun:test"; import { runTestCode } from "./helper"; describe("DockStatAPI (DELETE)", () => { - it("Delete all Logs /logs", async () => { - await runTestCode("/logs", 200, "DELETE", {}); - }); + it("Delete all Logs /logs", async () => { + await runTestCode("/logs", 200, "DELETE", {}); + }); - it("Delete Logs (Debug) /logs/debug", async () => { - await runTestCode("/logs/debug", 200, "DELETE", {}); - }); + it("Delete Logs (Debug) /logs/debug", async () => { + await runTestCode("/logs/debug", 200, "DELETE", {}); + }); }); diff --git a/src/tests/gets.spec.ts b/src/tests/gets.spec.ts index 7b103151..e542a0f4 100644 --- a/src/tests/gets.spec.ts +++ b/src/tests/gets.spec.ts @@ -1,61 +1,61 @@ import { describe, it } from "bun:test"; import { - version, - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; -import { runTestResponse, runTestCode } from "./helper"; +import { runTestCode, runTestResponse } from "./helper"; describe("DockStatAPI (GET)", () => { - it("Check Server connection", async () => { - await runTestResponse("/health", '{"status":"healthy"}', "GET"); - }); - - it("Check /docker/containers", async () => { - await runTestCode("/docker/containers", 200, "GET"); - }); - - it("Check /docker/hosts/Localhost", async () => { - await runTestCode("/docker/hosts/Localhost", 200, "GET"); - }); - - it("Check /docker-config/hosts", async () => { - await runTestCode("/docker-config/hosts", 200, "GET"); - }); - - it("Check /logs/", async () => { - await runTestCode("/logs", 200, "GET"); - }); - - it("Check /logs/debug", async () => { - await runTestCode("/logs/debug", 200, "GET"); - }); - - it("Check /config", async () => { - await runTestCode("/config", 200, "GET"); - }); - - it("Check /config/package", async () => { - const expected = JSON.stringify({ - version, - description, - license, - authorName, - authorEmail, - authorWebsite, - contributors, - dependencies, - devDependencies, - }); - - await runTestResponse("/config/package", expected, "GET"); - }); + it("Check Server connection", async () => { + await runTestResponse("/health", '{"status":"healthy"}', "GET"); + }); + + it("Check /docker/containers", async () => { + await runTestCode("/docker/containers", 200, "GET"); + }); + + it("Check /docker/hosts/Localhost", async () => { + await runTestCode("/docker/hosts/Localhost", 200, "GET"); + }); + + it("Check /docker-config/hosts", async () => { + await runTestCode("/docker-config/hosts", 200, "GET"); + }); + + it("Check /logs/", async () => { + await runTestCode("/logs", 200, "GET"); + }); + + it("Check /logs/debug", async () => { + await runTestCode("/logs/debug", 200, "GET"); + }); + + it("Check /config", async () => { + await runTestCode("/config", 200, "GET"); + }); + + it("Check /config/package", async () => { + const expected = JSON.stringify({ + version, + description, + license, + authorName, + authorEmail, + authorWebsite, + contributors, + dependencies, + devDependencies, + }); + + await runTestResponse("/config/package", expected, "GET"); + }); }); diff --git a/src/tests/helper.ts b/src/tests/helper.ts index 59ec0394..fabc45b3 100644 --- a/src/tests/helper.ts +++ b/src/tests/helper.ts @@ -8,114 +8,119 @@ export const API_KEY = "TestKey"; const server = "http://localhost:3001"; export async function runTestResponse( - path: string, - expected_response: any, - method?: "GET" | "POST" | "DELETE", - requestBody?: any + path: string, + expected_response: string, + method: "GET" | "POST" | "DELETE" = "GET", + requestBody?: string, ) { - method = method || "GET"; - const route = `${server}${path}`; - - logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); - const startTime = Date.now(); - - try { - const processedBody = - requestBody !== undefined - ? typeof requestBody === "string" - ? requestBody - : JSON.stringify(requestBody) - : undefined; - - const request = new Request(route, { - method, - body: processedBody, - headers: { - "Content-Type": "application/json", - "x-api-key": API_KEY, - }, - }); - - logger.debug( - `Request details: ${JSON.stringify({ - url: route, - method, - headers: [...request.headers], - body: processedBody, - })}` - ); - - const response = await DockStatAPI.handle(request); - const headers: { [key: string]: string } = {}; - response.headers.forEach((value, key) => (headers[key] = value)); - - const responseText = await response.text(); - const duration = Date.now() - startTime; - - logger.debug(`Received HTTP status: ${response.status}`); - logger.debug(`Response headers: ${JSON.stringify(headers)}`); - logger.debug(`Response body: ${responseText}`); - logger.debug(`Total Duration: ${duration}ms`); - - expect(responseText).toBe(expected_response); - logger.info(`__UT__ [ END ] Completed test on ${route}`); - } catch (error) { - logger.error(`__UT__ Error during test on ${route}: ${error}`); - throw error; - } + const route = `${server}${path}`; + + logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); + const startTime = Date.now(); + + try { + const processedBody = + requestBody !== undefined + ? typeof requestBody === "string" + ? requestBody + : JSON.stringify(requestBody) + : undefined; + + const request = new Request(route, { + method, + body: processedBody, + headers: { + "Content-Type": "application/json", + "x-api-key": API_KEY, + }, + }); + + logger.debug( + `Request details: ${JSON.stringify({ + url: route, + method, + headers: [...request.headers], + body: processedBody, + })}`, + ); + + const response = await DockStatAPI.handle(request); + const headers: { [key: string]: string } = {}; + + response.headers.forEach((value, key) => { + headers[key] = value; + }); + + const responseText = await response.text(); + const duration = Date.now() - startTime; + + logger.debug(`Received HTTP status: ${response.status}`); + logger.debug(`Response headers: ${JSON.stringify(headers)}`); + logger.debug(`Response body: ${responseText}`); + logger.debug(`Total Duration: ${duration}ms`); + + expect(responseText).toBe(expected_response); + logger.info(`__UT__ [ END ] Completed test on ${route}`); + } catch (error) { + logger.error(`__UT__ Error during test on ${route}: ${error}`); + throw error; + } } export async function runTestCode( - path: string, - expected_code: number, - method?: "GET" | "POST" | "DELETE", - requestBody?: any + path: string, + expected_code: number, + method: "GET" | "POST" | "DELETE" = "GET", + requestBody?: object, ) { - method = method || "GET"; - const route = `${server}${path}`; - - logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); - const startTime = Date.now(); - - try { - const processedBody = - requestBody !== undefined - ? typeof requestBody === "string" - ? requestBody - : JSON.stringify(requestBody) - : undefined; - - const request = new Request(route, { - method, - body: processedBody, - headers: { - "Content-Type": "application/json", - "x-api-key": API_KEY, - }, - }); - - logger.debug( - `Request details: ${JSON.stringify({ - url: route, - method, - headers: [...request.headers], - body: processedBody, - })}` - ); - - const response = await DockStatAPI.handle(request); - const headers: { [key: string]: string } = {}; - response.headers.forEach((value, key) => (headers[key] = value)); - const duration = Date.now() - startTime; - - logger.debug(`Received HTTP status: ${response.status}`); - logger.debug(`Response headers: ${JSON.stringify(headers)}`); - logger.debug(`Response body: ${JSON.stringify(response.body)}`); - - expect(response.status).toBe(expected_code); - logger.debug(`__UT__ Completed test on ${route} (Duration: ${duration}ms)`); - } catch (error) { - logger.error(`__UT__ Error during test on ${route}: ${error}`); - throw error; - } + const route = `${server}${path}`; + + logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); + const startTime = Date.now(); + + try { + const processedBody = + requestBody !== undefined + ? typeof requestBody === "string" + ? requestBody + : JSON.stringify(requestBody) + : undefined; + + const request = new Request(route, { + method, + body: processedBody, + headers: { + "Content-Type": "application/json", + "x-api-key": API_KEY, + }, + }); + + logger.debug( + `Request details: ${JSON.stringify({ + url: route, + method, + headers: [...request.headers], + body: processedBody, + })}`, + ); + + const response = await DockStatAPI.handle(request); + const headers: { [key: string]: string } = {}; + + response.headers.forEach((value, key) => { + headers[key] = value; + }); + + const duration = Date.now() - startTime; + + logger.debug(`Received HTTP status: ${response.status}`); + logger.debug(`Response headers: ${JSON.stringify(headers)}`); + logger.debug(`Response body: ${JSON.stringify(response.body)}`); + + expect(response.status).toBe(expected_code); + logger.debug(`__UT__ Completed test on ${route} (Duration: ${duration}ms)`); + } catch (error) { + logger.error(`__UT__ Error during test on ${route}: ${error}`); + throw error; + } } diff --git a/src/tests/post.spec.ts b/src/tests/post.spec.ts index 9747e67f..a4933dcc 100644 --- a/src/tests/post.spec.ts +++ b/src/tests/post.spec.ts @@ -1,52 +1,52 @@ import { describe, it } from "bun:test"; -import { runTestResponse, runTestCode } from "./helper"; +import { runTestCode, runTestResponse } from "./helper"; -import { DockerHost } from "~/typings/docker"; +import type { DockerHost } from "~/typings/docker"; describe("DockStatAPI (POST)", () => { - it("Check Host adding", async () => { - const body = { - name: "test", - hostAddress: "localhost:2375", - secure: false, - }; - - await runTestCode("/docker-config/add-host", 200, "POST", body); - await runTestCode("/docker-config/hosts", 200, "GET"); - }); - - it("Check Host Updating", async () => { - const codeBody: DockerHost = { - id: 2, - name: "test", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - await runTestCode("/docker-config/update-host", 200, "POST", codeBody); - - const responseBody: DockerHost[] = [ - { id: 2, name: "test", hostAddress: "127.0.0.1:2375", secure: false }, - { - id: 1, - name: "Localhost", - hostAddress: "localhost:2375", - secure: false, - }, - ]; - await runTestResponse( - "/docker-config/hosts", - JSON.stringify(responseBody), - "GET" - ); - }); - - it("Check Config update", async () => { - await runTestCode("/config/update", 200, "POST", { - fetching_interval: 1, - keep_data_for: 1, - api_key: "TestKey", - }); - }); + it("Check Host adding", async () => { + const body = { + name: "test", + hostAddress: "localhost:2375", + secure: false, + }; + + await runTestCode("/docker-config/add-host", 200, "POST", body); + await runTestCode("/docker-config/hosts", 200, "GET"); + }); + + it("Check Host Updating", async () => { + const codeBody: DockerHost = { + id: 2, + name: "test", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + + await runTestCode("/docker-config/update-host", 200, "POST", codeBody); + + const responseBody: DockerHost[] = [ + { id: 2, name: "test", hostAddress: "127.0.0.1:2375", secure: false }, + { + id: 1, + name: "Localhost", + hostAddress: "localhost:2375", + secure: false, + }, + ]; + await runTestResponse( + "/docker-config/hosts", + JSON.stringify(responseBody), + "GET", + ); + }); + + it("Check Config update", async () => { + await runTestCode("/config/update", 200, "POST", { + fetching_interval: 1, + keep_data_for: 1, + api_key: "TestKey", + }); + }); }); diff --git a/src/typings/database.ts b/src/typings/database.ts index 67d8121a..880a801c 100644 --- a/src/typings/database.ts +++ b/src/typings/database.ts @@ -1,27 +1,27 @@ interface config { - keep_data_for: number; - fetching_interval: number; - api_key: string; + keep_data_for: number; + fetching_interval: number; + api_key: string; } interface stacks_config { - id: number; - name: string; - version: number; - custom: boolean; - source: string; - container_count: number; - stack_prefix: string; - automatic_reboot_on_error: boolean; - image_updates: boolean; + id: number; + name: string; + version: number; + custom: boolean; + source: string; + container_count: number; + stack_prefix: string; + automatic_reboot_on_error: boolean; + image_updates: boolean; } interface log_message { - level: string; - timestamp: string; - message: string; - file: string; - line: number; + level: string; + timestamp: string; + message: string; + file: string; + line: number; } export type { config, stacks_config, log_message }; diff --git a/src/typings/docker-compose.ts b/src/typings/docker-compose.ts index 9067abaa..8e6a5f93 100644 --- a/src/typings/docker-compose.ts +++ b/src/typings/docker-compose.ts @@ -1,440 +1,522 @@ export interface Stack { - compose_spec: ComposeSpec; - name: string; - version: number; - source: string; - id?: number; + compose_spec: ComposeSpec; + name: string; + version: number; + source: string; + id?: number; } export interface ComposeSpec { - version?: string; - name?: string; - include?: Include[]; - services?: { [key: string]: Service }; - networks?: { [key: string]: Network }; - volumes?: { [key: string]: Volume }; - secrets?: { [key: string]: Secret }; - configs?: { [key: string]: Config }; - [key: `x-${string}`]: any; + version?: string; + name?: string; + include?: Include[]; + services?: { [key: string]: Service }; + networks?: { [key: string]: Network }; + volumes?: { [key: string]: Volume }; + secrets?: { [key: string]: Secret }; + configs?: { [key: string]: Config }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } type Include = - | string - | { - path: string | string[]; - env_file?: string | string[]; - project_directory?: string; - }; + | string + | { + path: string | string[]; + env_file?: string | string[]; + project_directory?: string; + }; interface Service { - develop?: Development | null; - deploy?: Deployment | null; - annotations?: ListOrDict; - attach?: boolean | string; - build?: - | string - | { - context?: string; - dockerfile?: string; - dockerfile_inline?: string; - entitlements?: string[]; - args?: ListOrDict; - ssh?: ListOrDict; - labels?: ListOrDict; - cache_from?: string[]; - cache_to?: string[]; - no_cache?: boolean | string; - additional_contexts?: ListOrDict; - network?: string; - pull?: boolean | string; - target?: string; - shm_size?: number | string; - extra_hosts?: ExtraHosts; - isolation?: string; - privileged?: boolean | string; - secrets?: ServiceConfigOrSecret[]; - tags?: string[]; - ulimits?: Ulimits; - platforms?: string[]; - [key: `x-${string}`]: any; - }; - blkio_config?: { - device_read_bps?: BlkioLimit[]; - device_read_iops?: BlkioLimit[]; - device_write_bps?: BlkioLimit[]; - device_write_iops?: BlkioLimit[]; - weight?: number | string; - weight_device?: BlkioWeight[]; - }; - cap_add?: string[]; - cap_drop?: string[]; - cgroup?: "host" | "private"; - cgroup_parent?: string; - command?: Command; - configs?: ServiceConfigOrSecret[]; - container_name?: string; - cpu_count?: string | number; - cpu_percent?: string | number; - cpu_shares?: number | string; - cpu_quota?: number | string; - cpu_period?: number | string; - cpu_rt_period?: number | string; - cpu_rt_runtime?: number | string; - cpus?: number | string; - cpuset?: string; - credential_spec?: { - config?: string; - file?: string; - registry?: string; - [key: `x-${string}`]: any; - }; - depends_on?: - | string[] - | { - [service: string]: { - condition: - | "service_started" - | "service_healthy" - | "service_completed_successfully"; - restart?: boolean | string; - required?: boolean; - [key: `x-${string}`]: any; - }; - }; - device_cgroup_rules?: string[]; - devices?: ( - | string - | { - source: string; - target?: string; - permissions?: string; - [key: `x-${string}`]: any; - } - )[]; - dns?: StringOrList; - dns_opt?: string[]; - dns_search?: StringOrList; - domainname?: string; - entrypoint?: Command; - env_file?: EnvFile; - label_file?: string | string[]; - environment?: ListOrDict; - expose?: (string | number)[]; - extends?: string | { service: string; file?: string }; - external_links?: string[]; - extra_hosts?: ExtraHosts; - gpus?: - | "all" - | Array<{ - capabilities?: string[]; - count?: string | number; - device_ids?: string[]; - driver?: string; - options?: ListOrDict; - [key: `x-${string}`]: any; - }>; - group_add?: (string | number)[]; - healthcheck?: Healthcheck; - hostname?: string; - image?: string; - init?: boolean | string; - ipc?: string; - isolation?: string; - labels?: ListOrDict; - links?: string[]; - logging?: { - driver?: string; - options?: { [key: string]: string | number | null }; - [key: `x-${string}`]: any; - }; - mac_address?: string; - mem_limit?: number | string; - mem_reservation?: string | number; - mem_swappiness?: number | string; - memswap_limit?: number | string; - network_mode?: string; - networks?: - | string[] - | { - [network: string]: { - aliases?: string[]; - ipv4_address?: string; - ipv6_address?: string; - link_local_ips?: string[]; - mac_address?: string; - driver_opts?: { [key: string]: string | number }; - priority?: number; - [key: `x-${string}`]: any; - } | null; - }; - oom_kill_disable?: boolean | string; - oom_score_adj?: string | number; - pid?: string | null; - pids_limit?: number | string; - platform?: string; - ports?: ( - | number - | string - | { - name?: string; - mode?: string; - host_ip?: string; - target?: number | string; - published?: string | number; - protocol?: string; - app_protocol?: string; - [key: `x-${string}`]: any; - } - )[]; - post_start?: ServiceHook[]; - pre_stop?: ServiceHook[]; - privileged?: boolean | string; - profiles?: string[]; - pull_policy?: "always" | "never" | "if_not_present" | "build" | "missing"; - read_only?: boolean | string; - restart?: string; - runtime?: string; - scale?: number | string; - security_opt?: string[]; - shm_size?: number | string; - secrets?: ServiceConfigOrSecret[]; - sysctls?: ListOrDict; - stdin_open?: boolean | string; - stop_grace_period?: string; - stop_signal?: string; - storage_opt?: object; - tmpfs?: StringOrList; - tty?: boolean | string; - ulimits?: Ulimits; - user?: string; - uts?: string; - userns_mode?: string; - volumes?: ( - | string - | { - type: string; - source?: string; - target?: string; - read_only?: boolean | string; - consistency?: string; - bind?: { - propagation?: string; - create_host_path?: boolean | string; - recursive?: "enabled" | "disabled" | "writable" | "readonly"; - selinux?: "z" | "Z"; - [key: `x-${string}`]: any; - }; - volume?: { - nocopy?: boolean | string; - subpath?: string; - [key: `x-${string}`]: any; - }; - tmpfs?: { - size?: number | string; - mode?: number | string; - [key: `x-${string}`]: any; - }; - [key: `x-${string}`]: any; - } - )[]; - volumes_from?: string[]; - working_dir?: string; - [key: `x-${string}`]: any; + develop?: Development | null; + deploy?: Deployment | null; + annotations?: ListOrDict; + attach?: boolean | string; + build?: + | string + | { + context?: string; + dockerfile?: string; + dockerfile_inline?: string; + entitlements?: string[]; + args?: ListOrDict; + ssh?: ListOrDict; + labels?: ListOrDict; + cache_from?: string[]; + cache_to?: string[]; + no_cache?: boolean | string; + additional_contexts?: ListOrDict; + network?: string; + pull?: boolean | string; + target?: string; + shm_size?: number | string; + extra_hosts?: ExtraHosts; + isolation?: string; + privileged?: boolean | string; + secrets?: ServiceConfigOrSecret[]; + tags?: string[]; + ulimits?: Ulimits; + platforms?: string[]; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + blkio_config?: { + device_read_bps?: BlkioLimit[]; + device_read_iops?: BlkioLimit[]; + device_write_bps?: BlkioLimit[]; + device_write_iops?: BlkioLimit[]; + weight?: number | string; + weight_device?: BlkioWeight[]; + }; + cap_add?: string[]; + cap_drop?: string[]; + cgroup?: "host" | "private"; + cgroup_parent?: string; + command?: Command; + configs?: ServiceConfigOrSecret[]; + container_name?: string; + cpu_count?: string | number; + cpu_percent?: string | number; + cpu_shares?: number | string; + cpu_quota?: number | string; + cpu_period?: number | string; + cpu_rt_period?: number | string; + cpu_rt_runtime?: number | string; + cpus?: number | string; + cpuset?: string; + credential_spec?: { + config?: string; + file?: string; + registry?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + depends_on?: + | string[] + | { + [service: string]: { + condition: + | "service_started" + | "service_healthy" + | "service_completed_successfully"; + restart?: boolean | string; + required?: boolean; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + }; + device_cgroup_rules?: string[]; + devices?: ( + | string + | { + source: string; + target?: string; + permissions?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + } + )[]; + dns?: StringOrList; + dns_opt?: string[]; + dns_search?: StringOrList; + domainname?: string; + entrypoint?: Command; + env_file?: EnvFile; + label_file?: string | string[]; + environment?: ListOrDict; + expose?: (string | number)[]; + extends?: string | { service: string; file?: string }; + external_links?: string[]; + extra_hosts?: ExtraHosts; + gpus?: + | "all" + | Array<{ + capabilities?: string[]; + count?: string | number; + device_ids?: string[]; + driver?: string; + options?: ListOrDict; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }>; + group_add?: (string | number)[]; + healthcheck?: Healthcheck; + hostname?: string; + image?: string; + init?: boolean | string; + ipc?: string; + isolation?: string; + labels?: ListOrDict; + links?: string[]; + logging?: { + driver?: string; + options?: { [key: string]: string | number | null }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + mac_address?: string; + mem_limit?: number | string; + mem_reservation?: string | number; + mem_swappiness?: number | string; + memswap_limit?: number | string; + network_mode?: string; + networks?: + | string[] + | { + [network: string]: { + aliases?: string[]; + ipv4_address?: string; + ipv6_address?: string; + link_local_ips?: string[]; + mac_address?: string; + driver_opts?: { [key: string]: string | number }; + priority?: number; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + } | null; + }; + oom_kill_disable?: boolean | string; + oom_score_adj?: string | number; + pid?: string | null; + pids_limit?: number | string; + platform?: string; + ports?: ( + | number + | string + | { + name?: string; + mode?: string; + host_ip?: string; + target?: number | string; + published?: string | number; + protocol?: string; + app_protocol?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + } + )[]; + post_start?: ServiceHook[]; + pre_stop?: ServiceHook[]; + privileged?: boolean | string; + profiles?: string[]; + pull_policy?: "always" | "never" | "if_not_present" | "build" | "missing"; + read_only?: boolean | string; + restart?: string; + runtime?: string; + scale?: number | string; + security_opt?: string[]; + shm_size?: number | string; + secrets?: ServiceConfigOrSecret[]; + sysctls?: ListOrDict; + stdin_open?: boolean | string; + stop_grace_period?: string; + stop_signal?: string; + storage_opt?: object; + tmpfs?: StringOrList; + tty?: boolean | string; + ulimits?: Ulimits; + user?: string; + uts?: string; + userns_mode?: string; + volumes?: ( + | string + | { + type: string; + source?: string; + target?: string; + read_only?: boolean | string; + consistency?: string; + bind?: { + propagation?: string; + create_host_path?: boolean | string; + recursive?: "enabled" | "disabled" | "writable" | "readonly"; + selinux?: "z" | "Z"; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + volume?: { + nocopy?: boolean | string; + subpath?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + tmpfs?: { + size?: number | string; + mode?: number | string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + } + )[]; + volumes_from?: string[]; + working_dir?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } interface Healthcheck { - disable?: boolean | string; - interval?: string; - retries?: number | string; - test?: string | string[]; - timeout?: string; - start_period?: string; - start_interval?: string; - [key: `x-${string}`]: any; + disable?: boolean | string; + interval?: string; + retries?: number | string; + test?: string | string[]; + timeout?: string; + start_period?: string; + start_interval?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } interface Development { - watch?: Array<{ - path: string; - action: "rebuild" | "sync" | "restart" | "sync+restart" | "sync+exec"; - ignore?: string[]; - target?: string; - exec?: ServiceHook; - [key: `x-${string}`]: any; - }>; - [key: `x-${string}`]: any; + watch?: Array<{ + path: string; + action: "rebuild" | "sync" | "restart" | "sync+restart" | "sync+exec"; + ignore?: string[]; + target?: string; + exec?: ServiceHook; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }>; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } interface Deployment { - mode?: string; - endpoint_mode?: string; - replicas?: number | string; - labels?: ListOrDict; - rollback_config?: { - parallelism?: number | string; - delay?: string; - failure_action?: string; - monitor?: string; - max_failure_ratio?: number | string; - order?: "start-first" | "stop-first"; - [key: `x-${string}`]: any; - }; - update_config?: { - parallelism?: number | string; - delay?: string; - failure_action?: string; - monitor?: string; - max_failure_ratio?: number | string; - order?: "start-first" | "stop-first"; - [key: `x-${string}`]: any; - }; - resources?: { - limits?: { - cpus?: number | string; - memory?: string; - pids?: number | string; - [key: `x-${string}`]: any; - }; - reservations?: { - cpus?: number | string; - memory?: string; - generic_resources?: Array<{ - discrete_resource_spec?: { - kind?: string; - value?: number | string; - [key: `x-${string}`]: any; - }; - [key: `x-${string}`]: any; - }>; - devices?: Array<{ - capabilities?: string[]; - count?: string | number; - device_ids?: string[]; - driver?: string; - options?: ListOrDict; - [key: `x-${string}`]: any; - }>; - [key: `x-${string}`]: any; - }; - [key: `x-${string}`]: any; - }; - restart_policy?: { - condition?: string; - delay?: string; - max_attempts?: number | string; - window?: string; - [key: `x-${string}`]: any; - }; - placement?: { - constraints?: string[]; - preferences?: Array<{ - spread?: string; - [key: `x-${string}`]: any; - }>; - max_replicas_per_node?: number | string; - [key: `x-${string}`]: any; - }; - [key: `x-${string}`]: any; + mode?: string; + endpoint_mode?: string; + replicas?: number | string; + labels?: ListOrDict; + rollback_config?: { + parallelism?: number | string; + delay?: string; + failure_action?: string; + monitor?: string; + max_failure_ratio?: number | string; + order?: "start-first" | "stop-first"; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + update_config?: { + parallelism?: number | string; + delay?: string; + failure_action?: string; + monitor?: string; + max_failure_ratio?: number | string; + order?: "start-first" | "stop-first"; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + resources?: { + limits?: { + cpus?: number | string; + memory?: string; + pids?: number | string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + reservations?: { + cpus?: number | string; + memory?: string; + generic_resources?: Array<{ + discrete_resource_spec?: { + kind?: string; + value?: number | string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }>; + devices?: Array<{ + capabilities?: string[]; + count?: string | number; + device_ids?: string[]; + driver?: string; + options?: ListOrDict; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }>; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + restart_policy?: { + condition?: string; + delay?: string; + max_attempts?: number | string; + window?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + placement?: { + constraints?: string[]; + preferences?: Array<{ + spread?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }>; + max_replicas_per_node?: number | string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } type Command = string | string[] | null; type EnvFile = - | string - | Array< - string | { path: string; format?: string; required?: boolean | string } - >; + | string + | Array< + string | { path: string; format?: string; required?: boolean | string } + >; type StringOrList = string | string[]; type ListOrDict = - | { [key: string]: string | number | boolean | null } - | string[]; + | { [key: string]: string | number | boolean | null } + | string[]; type ExtraHosts = { [host: string]: string | string[] } | string[]; interface BlkioLimit { - path: string; - rate: number | string; + path: string; + rate: number | string; } interface BlkioWeight { - path: string; - weight: number | string; + path: string; + weight: number | string; } type ServiceConfigOrSecret = - | string - | { - source: string; - target?: string; - uid?: string; - gid?: string; - mode?: number | string; - [key: `x-${string}`]: any; - }; + | string + | { + source: string; + target?: string; + uid?: string; + gid?: string; + mode?: number | string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; type Ulimits = { - [key: string]: - | number - | string - | { hard: number | string; soft: number | string }; + [key: string]: + | number + | string + | { hard: number | string; soft: number | string }; }; interface ServiceHook { - command?: Command; - user?: string; - privileged?: boolean | string; - working_dir?: string; - environment?: ListOrDict; - [key: `x-${string}`]: any; + command?: Command; + user?: string; + privileged?: boolean | string; + working_dir?: string; + environment?: ListOrDict; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } interface Network { - name?: string; - driver?: string; - driver_opts?: { [key: string]: string | number }; - ipam?: { - driver?: string; - config?: Array<{ - subnet?: string; - ip_range?: string; - gateway?: string; - aux_addresses?: { [key: string]: string }; - [key: `x-${string}`]: any; - }>; - options?: { [key: string]: string }; - [key: `x-${string}`]: any; - }; - external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; - internal?: boolean | string; - enable_ipv4?: boolean | string; - enable_ipv6?: boolean | string; - attachable?: boolean | string; - labels?: ListOrDict; - [key: `x-${string}`]: any; + name?: string; + driver?: string; + driver_opts?: { [key: string]: string | number }; + ipam?: { + driver?: string; + config?: Array<{ + subnet?: string; + ip_range?: string; + gateway?: string; + aux_addresses?: { [key: string]: string }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }>; + options?: { [key: string]: string }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; + }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; + internal?: boolean | string; + enable_ipv4?: boolean | string; + enable_ipv6?: boolean | string; + attachable?: boolean | string; + labels?: ListOrDict; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } interface Volume { - name?: string; - driver?: string; - driver_opts?: { [key: string]: string | number }; - external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; - labels?: ListOrDict; - [key: `x-${string}`]: any; + name?: string; + driver?: string; + driver_opts?: { [key: string]: string | number }; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; + labels?: ListOrDict; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } interface Secret { - name?: string; - environment?: string; - file?: string; - external?: boolean | string | { name?: string; [key: string]: any }; - labels?: ListOrDict; - driver?: string; - driver_opts?: { [key: string]: string | number }; - template_driver?: string; - [key: `x-${string}`]: any; + name?: string; + environment?: string; + file?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + external?: boolean | string | { name?: string; [key: string]: any }; + labels?: ListOrDict; + driver?: string; + driver_opts?: { [key: string]: string | number }; + template_driver?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } interface Config { - name?: string; - content?: string; - environment?: string; - file?: string; - external?: boolean | string | { name?: string; [key: string]: any }; - labels?: ListOrDict; - template_driver?: string; - [key: `x-${string}`]: any; + name?: string; + content?: string; + environment?: string; + file?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + external?: boolean | string | { name?: string; [key: string]: any }; + labels?: ListOrDict; + template_driver?: string; + + //biome-ignore lint/suspicious/noExplicitAny: Compose Spec + [key: `x-${string}`]: any; } diff --git a/src/typings/docker.ts b/src/typings/docker.ts index 8f8844ba..7e3b01f6 100644 --- a/src/typings/docker.ts +++ b/src/typings/docker.ts @@ -1,41 +1,41 @@ -import { ContainerStats } from "dockerode"; -import Docker from "dockerode"; +import type { ContainerStats } from "dockerode"; +import type Docker from "dockerode"; interface DockerHost { - name: string; - hostAddress: string; - secure: boolean; - id: number; + name: string; + hostAddress: string; + secure: boolean; + id: number; } interface ContainerInfo { - id: string; - hostId: string; - name: string; - image: string; - status: string; - state: string; - cpuUsage: number; - memoryUsage: number; - stats: ContainerStats; - info: Docker.ContainerInfo; + id: string; + hostId: string; + name: string; + image: string; + status: string; + state: string; + cpuUsage: number; + memoryUsage: number; + stats?: ContainerStats; + info?: Docker.ContainerInfo; } interface HostStats { - hostName: string; - hostId: number; - dockerVersion: string; - apiVersion: string; - os: string; - architecture: string; - totalMemory: number; - totalCPU: number; - labels: string[]; - containers: number; - containersRunning: number; - containersStopped: number; - containersPaused: number; - images: number; + hostName: string; + hostId: number; + dockerVersion: string; + apiVersion: string; + os: string; + architecture: string; + totalMemory: number; + totalCPU: number; + labels: string[]; + containers: number; + containersRunning: number; + containersStopped: number; + containersPaused: number; + images: number; } export type { HostStats, ContainerInfo, DockerHost }; diff --git a/src/typings/dockerode.ts b/src/typings/dockerode.ts index a4604337..e1268ad6 100644 --- a/src/typings/dockerode.ts +++ b/src/typings/dockerode.ts @@ -1,162 +1,162 @@ interface DockerInfo { - ID: string; - Containers: number; - ContainersRunning: number; - ContainersPaused: number; - ContainersStopped: number; - Images: number; - Driver: string; - DriverStatus: [string, string][]; - DockerRootDir: string; - SystemStatus: [string, string][]; - Plugins: { - Volume: string[]; - Network: string[]; - Authorization: string[]; - Log: string[]; - }; - MemoryLimit: boolean; - SwapLimit: boolean; - KernelMemory: boolean; - CpuCfsPeriod: boolean; - CpuCfsQuota: boolean; - CPUShares: boolean; - CPUSet: boolean; - OomKillDisable: boolean; - IPv4Forwarding: boolean; - BridgeNfIptables: boolean; - BridgeNfIp6tables: boolean; - Debug: boolean; - NFd: number; - NGoroutines: number; - SystemTime: string; - LoggingDriver: string; - CgroupDriver: string; - NEventsListener: number; - KernelVersion: string; - OperatingSystem: string; - OSType: string; - Architecture: string; - NCPU: number; - MemTotal: number; - IndexServerAddress: string; - RegistryConfig: { - AllowNondistributableArtifactsCIDRs: string[]; - AllowNondistributableArtifactsHostnames: string[]; - InsecureRegistryCIDRs: string[]; - IndexConfigs: Record< - string, - { - Name: string; - Mirrors: string[]; - Secure: boolean; - Official: boolean; - } - >; - Mirrors: string[]; - }; - GenericResources: Array< - | { DiscreteResourceSpec: { Kind: string; Value: number } } - | { NamedResourceSpec: { Kind: string; Value: string } } - >; - HttpProxy: string; - HttpsProxy: string; - NoProxy: string; - Name: string; - Labels: string[]; - ExperimentalBuild: boolean; - ServerVersion: string; - ClusterStore: string; - ClusterAdvertise: string; - Runtimes: Record< - string, - { - path: string; - runtimeArgs?: string[]; - } - >; - DefaultRuntime: string; - Swarm: { - NodeID: string; - NodeAddr: string; - LocalNodeState: string; - ControlAvailable: boolean; - Error: string; - RemoteManagers: Array<{ - NodeID: string; - Addr: string; - }>; - Nodes: number; - Managers: number; - Cluster: { - ID: string; - Version: { - Index: number; - }; - CreatedAt: string; - UpdatedAt: string; - Spec: { - Name: string; - Labels: Record; - Orchestration: { - TaskHistoryRetentionLimit: number; - }; - Raft: { - SnapshotInterval: number; - KeepOldSnapshots: number; - LogEntriesForSlowFollowers: number; - ElectionTick: number; - HeartbeatTick: number; - }; - Dispatcher: { - HeartbeatPeriod: number; - }; - CAConfig: { - NodeCertExpiry: number; - ExternalCAs: Array<{ - Protocol: string; - URL: string; - Options: Record; - CACert: string; - }>; - SigningCACert: string; - SigningCAKey: string; - ForceRotate: number; - }; - EncryptionConfig: { - AutoLockManagers: boolean; - }; - TaskDefaults: { - LogDriver: { - Name: string; - Options: Record; - }; - }; - }; - TLSInfo: { - TrustRoot: string; - CertIssuerSubject: string; - CertIssuerPublicKey: string; - }; - RootRotationInProgress: boolean; - }; - }; - LiveRestoreEnabled: boolean; - Isolation: string; - InitBinary: string; - ContainerdCommit: { - ID: string; - Expected: string; - }; - RuncCommit: { - ID: string; - Expected: string; - }; - InitCommit: { - ID: string; - Expected: string; - }; - SecurityOptions: string[]; + ID: string; + Containers: number; + ContainersRunning: number; + ContainersPaused: number; + ContainersStopped: number; + Images: number; + Driver: string; + DriverStatus: [string, string][]; + DockerRootDir: string; + SystemStatus: [string, string][]; + Plugins: { + Volume: string[]; + Network: string[]; + Authorization: string[]; + Log: string[]; + }; + MemoryLimit: boolean; + SwapLimit: boolean; + KernelMemory: boolean; + CpuCfsPeriod: boolean; + CpuCfsQuota: boolean; + CPUShares: boolean; + CPUSet: boolean; + OomKillDisable: boolean; + IPv4Forwarding: boolean; + BridgeNfIptables: boolean; + BridgeNfIp6tables: boolean; + Debug: boolean; + NFd: number; + NGoroutines: number; + SystemTime: string; + LoggingDriver: string; + CgroupDriver: string; + NEventsListener: number; + KernelVersion: string; + OperatingSystem: string; + OSType: string; + Architecture: string; + NCPU: number; + MemTotal: number; + IndexServerAddress: string; + RegistryConfig: { + AllowNondistributableArtifactsCIDRs: string[]; + AllowNondistributableArtifactsHostnames: string[]; + InsecureRegistryCIDRs: string[]; + IndexConfigs: Record< + string, + { + Name: string; + Mirrors: string[]; + Secure: boolean; + Official: boolean; + } + >; + Mirrors: string[]; + }; + GenericResources: Array< + | { DiscreteResourceSpec: { Kind: string; Value: number } } + | { NamedResourceSpec: { Kind: string; Value: string } } + >; + HttpProxy: string; + HttpsProxy: string; + NoProxy: string; + Name: string; + Labels: string[]; + ExperimentalBuild: boolean; + ServerVersion: string; + ClusterStore: string; + ClusterAdvertise: string; + Runtimes: Record< + string, + { + path: string; + runtimeArgs?: string[]; + } + >; + DefaultRuntime: string; + Swarm: { + NodeID: string; + NodeAddr: string; + LocalNodeState: string; + ControlAvailable: boolean; + Error: string; + RemoteManagers: Array<{ + NodeID: string; + Addr: string; + }>; + Nodes: number; + Managers: number; + Cluster: { + ID: string; + Version: { + Index: number; + }; + CreatedAt: string; + UpdatedAt: string; + Spec: { + Name: string; + Labels: Record; + Orchestration: { + TaskHistoryRetentionLimit: number; + }; + Raft: { + SnapshotInterval: number; + KeepOldSnapshots: number; + LogEntriesForSlowFollowers: number; + ElectionTick: number; + HeartbeatTick: number; + }; + Dispatcher: { + HeartbeatPeriod: number; + }; + CAConfig: { + NodeCertExpiry: number; + ExternalCAs: Array<{ + Protocol: string; + URL: string; + Options: Record; + CACert: string; + }>; + SigningCACert: string; + SigningCAKey: string; + ForceRotate: number; + }; + EncryptionConfig: { + AutoLockManagers: boolean; + }; + TaskDefaults: { + LogDriver: { + Name: string; + Options: Record; + }; + }; + }; + TLSInfo: { + TrustRoot: string; + CertIssuerSubject: string; + CertIssuerPublicKey: string; + }; + RootRotationInProgress: boolean; + }; + }; + LiveRestoreEnabled: boolean; + Isolation: string; + InitBinary: string; + ContainerdCommit: { + ID: string; + Expected: string; + }; + RuncCommit: { + ID: string; + Expected: string; + }; + InitCommit: { + ID: string; + Expected: string; + }; + SecurityOptions: string[]; } export type { DockerInfo }; diff --git a/src/typings/elysiajs.ts b/src/typings/elysiajs.ts index 913ceea1..a68bb8c5 100644 --- a/src/typings/elysiajs.ts +++ b/src/typings/elysiajs.ts @@ -1,12 +1,12 @@ import type { StatusMap } from "elysia"; -import type { HTTPHeaders } from "elysia/dist/types"; import type { ElysiaCookie } from "elysia/dist/cookies"; +import type { HTTPHeaders } from "elysia/dist/types"; interface set { - headers: HTTPHeaders; - status?: number | keyof StatusMap; - redirect?: string; - cookie?: Record; + headers: HTTPHeaders; + status?: number | keyof StatusMap; + redirect?: string; + cookie?: Record; } -export { set }; +export type { set }; diff --git a/src/typings/misc.ts b/src/typings/misc.ts new file mode 100644 index 00000000..c8bdc463 --- /dev/null +++ b/src/typings/misc.ts @@ -0,0 +1,5 @@ +export type BackupInfo = { + filename: string; + date: Date; + backupNum: number; +}; diff --git a/src/typings/plugin.ts b/src/typings/plugin.ts index 6ca68bf8..c5c3cc0f 100644 --- a/src/typings/plugin.ts +++ b/src/typings/plugin.ts @@ -1,26 +1,26 @@ -import { ContainerInfo } from "~/typings/docker"; +import type { ContainerInfo } from "~/typings/docker"; interface Plugin { - name: string; + name: string; - // Container lifecycle hooks - onContainerStart?: (containerInfo: ContainerInfo) => void; - onContainerStop?: (containerInfo: ContainerInfo) => void; - onContainerExit?: (containerInfo: ContainerInfo) => void; - onContainerCreate?: (containerInfo: ContainerInfo) => void; - onContainerKill?: (ContainerInfo: ContainerInfo) => void; - handleContainerDie?: (ContainerInfo: ContainerInfo) => void; - onContainerDestroy?: (containerInfo: ContainerInfo) => void; - onContainerPause?: (containerInfo: ContainerInfo) => void; - onContainerUnpause?: (containerInfo: ContainerInfo) => void; - onContainerRestart?: (containerInfo: ContainerInfo) => void; - onContainerUpdate?: (containerInfo: ContainerInfo) => void; - onContainerRename?: (containerInfo: ContainerInfo) => void; - onContainerHealthStatus?: (containerInfo: ContainerInfo) => void; + // Container lifecycle hooks + onContainerStart?: (containerInfo: ContainerInfo) => void; + onContainerStop?: (containerInfo: ContainerInfo) => void; + onContainerExit?: (containerInfo: ContainerInfo) => void; + onContainerCreate?: (containerInfo: ContainerInfo) => void; + onContainerKill?: (ContainerInfo: ContainerInfo) => void; + handleContainerDie?: (ContainerInfo: ContainerInfo) => void; + onContainerDestroy?: (containerInfo: ContainerInfo) => void; + onContainerPause?: (containerInfo: ContainerInfo) => void; + onContainerUnpause?: (containerInfo: ContainerInfo) => void; + onContainerRestart?: (containerInfo: ContainerInfo) => void; + onContainerUpdate?: (containerInfo: ContainerInfo) => void; + onContainerRename?: (containerInfo: ContainerInfo) => void; + onContainerHealthStatus?: (containerInfo: ContainerInfo) => void; - // Host lifecycle hooks - onHostUnreachable?: (host: string, err: string) => void; - onHostReachableAgain?: (host: string) => void; + // Host lifecycle hooks + onHostUnreachable?: (host: string, err: string) => void; + onHostReachableAgain?: (host: string) => void; } export type { Plugin }; diff --git a/src/typings/websocket.ts b/src/typings/websocket.ts index 5635e3c8..e7ed96da 100644 --- a/src/typings/websocket.ts +++ b/src/typings/websocket.ts @@ -1,15 +1,15 @@ interface stackSocketMessage { - message?: string; - type?: "stack-progress" | "stack-error" | "stack-status" | "stack-removed"; - data?: stackSocketData; + message?: string; + type?: "stack-progress" | "stack-error" | "stack-status" | "stack-removed"; + data?: stackSocketData; } interface stackSocketData { - stack_id: number; - message: string; - action?: string; - status?: string; - timestamp?: string; + stack_id: number; + message: string; + action?: string; + status?: string; + timestamp?: string; } -export { stackSocketMessage }; +export type { stackSocketMessage }; diff --git a/tsconfig.json b/tsconfig.json index 9c2d5112..3a44e369 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,107 +1,107 @@ { - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ - "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - "outDir": "build/", - /* Modules */ - "module": "ES2022" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - "paths": { - "~/*": ["./src/*"] - } /* Specify a set of entries that re-map imports to additional lookup locations. */, - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": [ - "bun-types" - ] /* Specify type package names to be included without being referenced in a source file. */, - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - "resolveJsonModule": true /* Enable importing .json files. */, - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* Language and Environment */ + "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + "outDir": "build/", + /* Modules */ + "module": "ES2022" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + "paths": { + "~/*": ["./src/*"] + } /* Specify a set of entries that re-map imports to additional lookup locations. */, + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": [ + "bun-types" + ] /* Specify type package names to be included without being referenced in a source file. */, + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "resolveJsonModule": true /* Enable importing .json files. */, + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } } From a8e30550b5dcddad578a2f9f7664563b8a94ee42 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 16 Apr 2025 20:16:53 +0000 Subject: [PATCH 241/369] Update dependency graphs --- dependency-graph.mmd | 332 ++++----- dependency-graph.svg | 1541 ++++++++++++++++++++++-------------------- 2 files changed, 996 insertions(+), 877 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 1ba62544..bb9e74a9 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -10,208 +10,222 @@ subgraph 0["src"] 1["index.ts"] subgraph 2["routes"] 3["live-stacks.ts"] -P["live-logs.ts"] -1D["api-config.ts"] -1F["docker-manager.ts"] -1G["docker-stats.ts"] -1H["docker-websocket.ts"] -1J["logs.ts"] -1K["stacks.ts"] -1N["utils.ts"] +S["live-logs.ts"] +1G["api-config.ts"] +1I["docker-manager.ts"] +1J["docker-stats.ts"] +1K["docker-websocket.ts"] +1M["logs.ts"] +1N["stacks.ts"] +1Q["utils.ts"] end subgraph 4["core"] subgraph 5["utils"] 6["logger.ts"] -M["helpers.ts"] -10["calculations.ts"] -15["change-me-checker.ts"] -17["package-json.ts"] -19["swagger-readme.ts"] -1E["response-handler.ts"] +Q["helpers.ts"] +14["calculations.ts"] +18["change-me-checker.ts"] +1A["package-json.ts"] +1C["swagger-readme.ts"] +1H["response-handler.ts"] end subgraph 8["database"] -9["index.ts"] -A["config.ts"] -B["database.ts"] -D["helper.ts"] -E["containerStats.ts"] -F["dockerHosts.ts"] -I["hostStats.ts"] -J["logs.ts"] -L["stacks.ts"] +9["_dbState.ts"] +A["index.ts"] +B["backup.ts"] +D["database.ts"] +F["helper.ts"] +I["config.ts"] +J["containerStats.ts"] +K["dockerHosts.ts"] +M["hostStats.ts"] +N["logs.ts"] +P["stacks.ts"] end -subgraph Q["docker"] -R["monitor.ts"] -X["client.ts"] -Y["scheduler.ts"] -Z["store-container-stats.ts"] -11["store-host-stats.ts"] +subgraph U["docker"] +V["monitor.ts"] +11["client.ts"] +12["scheduler.ts"] +13["store-container-stats.ts"] +15["store-host-stats.ts"] end -subgraph T["plugins"] -U["plugin-manager.ts"] -13["loader.ts"] +subgraph X["plugins"] +Y["plugin-manager.ts"] +17["loader.ts"] end -subgraph 1L["stacks"] -1M["controller.ts"] +subgraph 1O["stacks"] +1P["controller.ts"] end end subgraph G["typings"] -H["docker.ts"] -K["websocket.ts"] -N["database.ts"] -O["docker-compose.ts"] -W["plugin.ts"] -12["dockerode.ts"] -1C["elysiajs.ts"] +H["misc.ts"] +L["docker.ts"] +O["database.ts"] +R["docker-compose.ts"] +T["websocket.ts"] +10["plugin.ts"] +16["dockerode.ts"] +1F["elysiajs.ts"] end -subgraph 1A["middleware"] -1B["auth.ts"] +subgraph 1D["middleware"] +1E["auth.ts"] end end 7["path"] -C["bun:sqlite"] -S["bun"] -V["events"] -subgraph 14["fs"] -16["promises"] +subgraph C["fs"] +19["promises"] end -18["package.json"] -1I["stream"] +E["bun:sqlite"] +W["bun"] +Z["events"] +1B["package.json"] +1L["stream"] 1-->3 -1-->9 -1-->R -1-->Y -1-->13 -1-->6 +1-->A +1-->V +1-->12 1-->17 -1-->19 -1-->1B -1-->1D -1-->1F +1-->6 +1-->1A +1-->1C +1-->1E 1-->1G -1-->1H -1-->P +1-->1I 1-->1J 1-->1K +1-->S +1-->1M 1-->1N -1-->N +1-->1Q +1-->O 3-->6 -3-->K +3-->T 6-->9 -6-->P -6-->N +6-->A +6-->S +6-->O 6-->7 -9-->A -9-->E -9-->B -9-->F -9-->I -9-->J -9-->L A-->B +A-->I +A-->J A-->D +A-->K +A-->M +A-->N +A-->P +B-->9 +B-->D +B-->F +B-->6 +B-->H B-->C -D-->6 -E-->B -E-->D -F-->B -F-->D -F-->H -I-->B +D-->E +D-->C +F-->9 +F-->6 I-->D -I-->H -J-->B +I-->F J-->D -J-->K -L-->M -L-->B -L-->D -L-->N -L-->O -M-->6 -P-->6 -P-->N -R-->U -R-->9 -R-->X -R-->6 -R-->H -R-->H -R-->S -U-->6 -U-->H -U-->W -U-->V -W-->H -X-->6 -X-->H -Y-->9 -Y-->Z -Y-->11 +J-->F +K-->D +K-->F +K-->L +M-->D +M-->F +M-->L +N-->D +N-->F +N-->O +P-->Q +P-->D +P-->F +P-->O +P-->R +Q-->6 +S-->6 +S-->O +V-->Y +V-->A +V-->11 +V-->6 +V-->L +V-->W Y-->6 -Y-->N -Z-->6 -Z-->9 -Z-->X -Z-->10 -11-->9 -11-->X -11-->M +Y-->L +Y-->10 +Y-->Z +10-->L 11-->6 -11-->H -11-->12 -13-->15 +11-->L +12-->A +12-->13 +12-->15 +12-->6 +12-->O 13-->6 -13-->U +13-->A +13-->11 13-->14 -13-->7 +15-->A +15-->11 +15-->Q 15-->6 +15-->L 15-->16 17-->18 -1B-->9 -1B-->6 -1B-->N -1B-->1C -1D-->9 -1D-->U -1D-->6 -1D-->17 -1D-->1E -1D-->1B -1D-->N +17-->6 +17-->Y +17-->C +17-->7 +18-->6 +18-->19 +1A-->1B +1E-->A 1E-->6 -1E-->1C -1F-->9 -1F-->6 -1F-->1E -1F-->H -1G-->9 -1G-->X -1G-->10 -1G-->M +1E-->O +1E-->1F +1G-->A +1G-->B +1G-->Y 1G-->6 +1G-->1A +1G-->1H 1G-->1E -1G-->H -1G-->12 -1H-->9 -1H-->X -1H-->10 +1G-->O +1G-->C 1H-->6 -1H-->1E -1H-->1I -1J-->9 +1H-->1F +1I-->A +1I-->6 +1I-->1H +1I-->L +1J-->A +1J-->11 +1J-->14 +1J-->Q 1J-->6 -1K-->9 -1K-->1M +1J-->1H +1J-->L +1J-->16 +1K-->A +1K-->11 +1K-->14 1K-->6 -1K-->1E -1M-->M -1M-->9 +1K-->1H +1K-->1L +1M-->A 1M-->6 -1M-->3 -1M-->N -1M-->O -1M-->16 -1N-->17 -1N-->1E +1N-->A +1N-->1P +1N-->6 +1N-->1H +1P-->Q +1P-->A +1P-->6 +1P-->3 +1P-->O +1P-->R +1P-->19 +1Q-->1A +1Q-->1H diff --git a/dependency-graph.svg b/dependency-graph.svg index 34974a2d..3aa239a0 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,72 +4,72 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_src/typings - -typings + +typings bun - -bun + +bun @@ -77,8 +77,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -86,8 +86,8 @@ events - -events + +events @@ -95,8 +95,8 @@ fs - -fs + +fs @@ -104,8 +104,8 @@ fs/promises - -promises + +promises @@ -113,8 +113,8 @@ package.json - -package.json + +package.json @@ -122,1249 +122,1354 @@ path - -path + +path - + -src/core/database/config.ts - - -config.ts +src/core/database/_dbState.ts + + +_dbState.ts - + -src/core/database/database.ts - - -database.ts +src/core/database/backup.ts + + +backup.ts - + + +src/core/database/backup.ts->fs + + + + -src/core/database/config.ts->src/core/database/database.ts - - +src/core/database/backup.ts->src/core/database/_dbState.ts + + - + -src/core/database/helper.ts - - -helper.ts +src/core/database/database.ts + + +database.ts - + -src/core/database/config.ts->src/core/database/helper.ts - - - - - - - -src/core/database/database.ts->bun:sqlite - - +src/core/database/backup.ts->src/core/database/database.ts + + - - -src/core/utils/logger.ts - - -logger.ts + + +src/core/database/helper.ts + + +helper.ts - - -src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + +src/core/database/backup.ts->src/core/database/helper.ts + + + + - - -src/core/database/containerStats.ts - - -containerStats.ts + + +src/core/utils/logger.ts + + +logger.ts - - -src/core/database/containerStats.ts->src/core/database/database.ts - - - - + -src/core/database/containerStats.ts->src/core/database/helper.ts - - - - +src/core/database/backup.ts->src/core/utils/logger.ts + + + + - - -src/core/database/dockerHosts.ts - - -dockerHosts.ts + + +src/typings/misc.ts + + +misc.ts - - -src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + +src/core/database/backup.ts->src/typings/misc.ts + + - - -src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + +src/core/database/database.ts->bun:sqlite + + - - -src/typings/docker.ts - - -docker.ts - + + +src/core/database/database.ts->fs + + + + +src/core/database/helper.ts->src/core/database/_dbState.ts + + - - -src/core/database/dockerHosts.ts->src/typings/docker.ts - - + + +src/core/database/helper.ts->src/core/utils/logger.ts + + + + - + src/core/utils/logger.ts->path - - + + + + + +src/core/utils/logger.ts->src/core/database/_dbState.ts + + - + src/core/database/index.ts - - -index.ts + + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + - + src/typings/database.ts - - -database.ts + + +database.ts - + src/core/utils/logger.ts->src/typings/database.ts - - + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + - + + +src/core/database/config.ts + + +config.ts + + + + + +src/core/database/config.ts->src/core/database/database.ts + + + + + +src/core/database/config.ts->src/core/database/helper.ts + + + + + + +src/core/database/containerStats.ts + + +containerStats.ts + + + + + +src/core/database/containerStats.ts->src/core/database/database.ts + + + + + +src/core/database/containerStats.ts->src/core/database/helper.ts + + + + + + + +src/core/database/dockerHosts.ts + + +dockerHosts.ts + + + + + +src/core/database/dockerHosts.ts->src/core/database/database.ts + + + + + +src/core/database/dockerHosts.ts->src/core/database/helper.ts + + + + + + + +src/typings/docker.ts + + +docker.ts + + + + + +src/core/database/dockerHosts.ts->src/typings/docker.ts + + + + + src/core/database/hostStats.ts - - -hostStats.ts + + +hostStats.ts - + src/core/database/hostStats.ts->src/core/database/database.ts - - + + - + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/hostStats.ts->src/typings/docker.ts - - + + - - -src/core/database/index.ts->src/core/database/config.ts - - - - + + +src/core/database/index.ts->src/core/database/backup.ts + + + + - + src/core/database/index.ts->src/core/database/database.ts - - + + + + + +src/core/database/index.ts->src/core/database/config.ts + + + + - + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + - + src/core/database/logs.ts - - -logs.ts + + +logs.ts - + src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + - + src/core/database/stacks.ts - - -stacks.ts + + +stacks.ts - + src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + - + src/core/database/logs.ts->src/core/database/database.ts - - + + - + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + - - -src/typings/websocket.ts - - -websocket.ts - - - - - -src/core/database/logs.ts->src/typings/websocket.ts - - + + +src/core/database/logs.ts->src/typings/database.ts + + - + src/core/database/stacks.ts->src/core/database/database.ts - - + + - + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + + + + +src/core/database/stacks.ts->src/typings/database.ts + + - + src/core/utils/helpers.ts - - -helpers.ts + + +helpers.ts - + src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - - - - -src/core/database/stacks.ts->src/typings/database.ts - - + + + + - + src/typings/docker-compose.ts - - -docker-compose.ts + + +docker-compose.ts - + src/core/database/stacks.ts->src/typings/docker-compose.ts - - + + - + src/core/utils/helpers.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/docker/client.ts - - -client.ts + + +client.ts - - -src/core/docker/client.ts->src/typings/docker.ts - - - - + src/core/docker/client.ts->src/core/utils/logger.ts - - + + + + + +src/core/docker/client.ts->src/typings/docker.ts + + - + src/core/docker/monitor.ts - - -monitor.ts + + +monitor.ts - + src/core/docker/monitor.ts->bun - - - - - -src/core/docker/monitor.ts->src/typings/docker.ts - - + + - + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + + + + +src/core/docker/monitor.ts->src/typings/docker.ts + + - + src/core/docker/monitor.ts->src/core/database/index.ts - - + + - + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + - + src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +plugin-manager.ts - + src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - - - - -src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + + + + +src/core/plugins/plugin-manager.ts->src/typings/docker.ts + + - + src/typings/plugin.ts - - -plugin.ts + + +plugin.ts - + src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + - + src/core/docker/scheduler.ts->src/typings/database.ts - - + + - + src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - - -calculations.ts + + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - - - - -src/core/docker/store-host-stats.ts->src/typings/docker.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + + + + +src/core/docker/store-host-stats.ts->src/typings/docker.ts + + - + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + src/typings/dockerode.ts - - -dockerode.ts + + +dockerode.ts - + src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/typings/plugin.ts->src/typings/docker.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - - - - -src/core/stacks/controller.ts->src/core/utils/helpers.ts - - + + - + src/core/stacks/controller.ts->src/typings/database.ts - - + + + + + +src/core/stacks/controller.ts->src/core/utils/helpers.ts + + - + src/core/stacks/controller.ts->src/typings/docker-compose.ts - - + + - + src/routes/live-stacks.ts - - -live-stacks.ts + + +live-stacks.ts - + src/core/stacks/controller.ts->src/routes/live-stacks.ts - - + + - + src/routes/live-stacks.ts->src/core/utils/logger.ts - - + + + + + +src/typings/websocket.ts + + +websocket.ts + + - + src/routes/live-stacks.ts->src/typings/websocket.ts - - + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->src/typings/database.ts - - + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + src/typings/elysiajs.ts - - -elysiajs.ts + + +elysiajs.ts - + src/core/utils/response-handler.ts->src/typings/elysiajs.ts - - + + - + src/core/utils/swagger-readme.ts - - -swagger-readme.ts + + +swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->src/typings/database.ts - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/routes/live-stacks.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + - + src/index.ts->src/core/utils/package-json.ts - - + + - + src/index.ts->src/core/utils/swagger-readme.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/utils.ts - - -utils.ts + + +utils.ts - + src/index.ts->src/routes/utils.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->src/typings/database.ts - - + + - + src/middleware/auth.ts->src/typings/elysiajs.ts - - + + + + + +src/routes/api-config.ts->fs + + + + + +src/routes/api-config.ts->src/core/database/backup.ts + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->src/typings/database.ts - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - - - - -src/routes/docker-manager.ts->src/typings/docker.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + + + + +src/routes/docker-manager.ts->src/typings/docker.ts + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - - - - -src/routes/docker-stats.ts->src/typings/docker.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + + + + +src/routes/docker-stats.ts->src/typings/docker.ts + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/helpers.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->src/typings/dockerode.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + - + src/routes/utils.ts->src/core/utils/package-json.ts - - + + - + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + From 82b28c8fb90dbbf8fb072b6527fe3aa40e85cce0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:17:53 +0200 Subject: [PATCH 242/369] CI/CD: Fix Lint --- .github/workflows/pipeline.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 4a4f895f..26ee3646 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -25,6 +25,8 @@ jobs: - name: Lint run: | + bun install + bun clean bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --reporter=github --fix src - name: Commit and Push Changes @@ -41,8 +43,6 @@ jobs: - name: Run Unit-tests run: | - bun install - bun clean bun test --reporter=junit --reporter-outfile=./bun.xml - name: Run Docker Build From 10c7d5b2c61c2d8d6177176cd44d757c2a986e9f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:18:42 +0200 Subject: [PATCH 243/369] CI/CD: Fix permissions --- .github/workflows/pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 26ee3646..8d2eb09f 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -7,7 +7,7 @@ on: types: [published, prereleased] permissions: - contents: read + contents: write packages: write jobs: From 2c636b09a9416f4394b2b5bbc60ca8dbc561209b Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 16 Apr 2025 20:19:06 +0000 Subject: [PATCH 244/369] Linting --- src/core/stacks/controller.ts | 560 +++++++++++++++++----------------- 1 file changed, 280 insertions(+), 280 deletions(-) diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 8416b357..b6f6ddde 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -9,339 +9,339 @@ import type { ComposeSpec, Stack } from "~/typings/docker-compose"; import { findObjectByKey } from "../utils/helpers"; const wrapProgressCallback = (progressCallback?: (log: string) => void) => { - return progressCallback - ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { - const log = chunk.toString(); - progressCallback(log); - } - : undefined; + return progressCallback + ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; }; async function getStackName(stack_id: number): Promise { - logger.debug(`Fetching stack name for id ${stack_id}`); - const stacks = dbFunctions.getStacks(); - const stack = findObjectByKey(stacks, "id", stack_id); - if (!stack) { - throw new Error(`Stack with id ${stack_id} not found`); - } - return stack.name; + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = findObjectByKey(stacks, "id", stack_id); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; } async function runStackCommand( - stack_id: number, - command: ( - cwd: string, - progressCallback?: (log: string) => void - ) => Promise, - action: string + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void, + ) => Promise, + action: string, ): Promise { - try { - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); + try { + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); - const progressCallback = (log: string) => { - postToClient({ - type: "stack-progress", - data: { - stack_id, - action, - message: log.trim(), - timestamp: new Date().toISOString(), - }, - }); - }; + const progressCallback = (log: string) => { + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message: log.trim(), + timestamp: new Date().toISOString(), + }, + }); + }; - return await command(stackPath, progressCallback); - } catch (error) { - postToClient({ - type: "stack-error", - data: { - stack_id, - action, - message: String(error), - timestamp: new Date().toISOString(), - }, - }); - throw new Error( - `Error while ${action} stack "${stack_id}": ${String(error)}` - ); - } + return await command(stackPath, progressCallback); + } catch (error) { + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: String(error), + timestamp: new Date().toISOString(), + }, + }); + throw new Error( + `Error while ${action} stack "${stack_id}": ${String(error)}`, + ); + } } async function getStackPath(stack: Stack): Promise { - const stackName = stack.name.trim().replace(/\s+/g, "_"); - return `stacks/${stackName}`; + const stackName = stack.name.trim().replace(/\s+/g, "_"); + return `stacks/${stackName}`; } async function createStackYAML(compose_spec: Stack): Promise { - const yaml = YAML.stringify(compose_spec.compose_spec); - const stackPath = await getStackPath(compose_spec); - await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { - createPath: true, - }); + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); } export async function deployStack( - stack: ComposeSpec, - name: string, - version: number, - source: string, - automatic_reboot_on_error: boolean, - isCustom: boolean, - image_updates: boolean, - stack_prefix?: string + stack: ComposeSpec, + name: string, + version: number, + source: string, + automatic_reboot_on_error: boolean, + isCustom: boolean, + image_updates: boolean, + stack_prefix?: string, ): Promise { - let stackId: number; + let stackId: number; - try { - logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`); - const serviceCount = stack.services - ? Object.keys(stack.services).length - : 0; - const resolvedPrefix = stack_prefix ?? ""; + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`); + const serviceCount = stack.services + ? Object.keys(stack.services).length + : 0; + const resolvedPrefix = stack_prefix ?? ""; - const stack_config: stacks_config = { - id: 0, - name, - version, - source, - stack_prefix: resolvedPrefix, - automatic_reboot_on_error, - container_count: serviceCount, - custom: isCustom, - image_updates, - }; + const stack_config: stacks_config = { + id: 0, + name, + version, + source, + stack_prefix: resolvedPrefix, + automatic_reboot_on_error, + container_count: serviceCount, + custom: isCustom, + image_updates, + }; - if (!name) { - throw new Error("Stack name needed"); - } + if (!name) { + throw new Error("Stack name needed"); + } - stackId = dbFunctions.addStack(stack_config) as number; - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "pending", - message: "Creating stack configuration", - }, - }); + stackId = dbFunctions.addStack(stack_config) as number; + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }); - const stackYaml: Stack = { - id: stackId, - name, - source, - version, - compose_spec: stack, - }; + const stackYaml: Stack = { + id: stackId, + name, + source, + version, + compose_spec: stack, + }; - await createStackYAML(stackYaml); + await createStackYAML(stackYaml); - await runStackCommand( - stackId, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "deploying" - ); + await runStackCommand( + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying", + ); - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "deployed", - message: "Stack deployed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id: 0, - action: "deploying", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id: 0, + action: "deploying", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } export async function stopStack(stack_id: number): Promise { - // Note the await to discard the result (convert to void) - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.downAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "stopping" - ); + // Note the await to discard the result (convert to void) + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.downAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping", + ); } export async function startStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "starting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting", + ); } export async function pullStackImages(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.pullAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "pulling-images" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images", + ); } export async function restartStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.restartAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "restarting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting", + ); } export async function getStackStatus( - stack_id: number - //biome-ignore lint/suspicious/noExplicitAny: + stack_id: number, + //biome-ignore lint/suspicious/noExplicitAny: ): Promise> { - const status = await runStackCommand( - stack_id, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - //biome-ignore lint/suspicious/noExplicitAny: - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check" - ); - return status; + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check", + ); + return status; } export async function removeStack(stack_id: number): Promise { - try { - await runStackCommand( - stack_id, - async (cwd, progressCallback) => { - await DockerCompose.down({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }); - }, - "removing" - ); + try { + await runStackCommand( + stack_id, + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }); + }, + "removing", + ); - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); - try { - await rm(stackPath, { recursive: true }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + try { + await rm(stackPath, { recursive: true }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } - dbFunctions.deleteStack(stack_id); - postToClient({ - type: "stack-removed", - data: { - stack_id, - message: "Stack removed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + dbFunctions.deleteStack(stack_id); + postToClient({ + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } //biome-ignore lint/suspicious/noExplicitAny: export async function getAllStacksStatus(): Promise> { - try { - const stacks = dbFunctions.getStacks(); + try { + const stacks = dbFunctions.getStacks(); - const statusResults = await Promise.all( - stacks.map(async (stack) => { - const status = await runStackCommand( - stack.id as number, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - //biome-ignore lint/suspicious/noExplicitAny: - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check" - ); - return { stackId: stack.id, status }; - }) - ); + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.id as number, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check", + ); + return { stackId: stack.id, status }; + }), + ); - return statusResults.reduce( - (acc, { stackId, status }) => { - // Ensure stackId is used as a string if necessary, e.g. - acc[String(stackId)] = status; - return acc; - }, - //biome-ignore lint/suspicious/noExplicitAny: - {} as Record - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + return statusResults.reduce( + (acc, { stackId, status }) => { + // Ensure stackId is used as a string if necessary, e.g. + acc[String(stackId)] = status; + return acc; + }, + //biome-ignore lint/suspicious/noExplicitAny: + {} as Record, + ); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } From 2cd11e4fb0d45fa0e8df641e1597fecae240734b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:20:10 +0200 Subject: [PATCH 245/369] CI/CD: Fix command --- .github/workflows/pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 8d2eb09f..dd91fb0c 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -27,7 +27,7 @@ jobs: run: | bun install bun clean - bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --reporter=github --fix src + bun lint -- --reporter=github - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 From 855959f6b2b0aaa67a3dddf67fd3247e98b9a9d1 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:25:43 +0200 Subject: [PATCH 246/369] CI/CD: Fix junit reports --- .github/workflows/pipeline.yaml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index dd91fb0c..4ae1baea 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -27,15 +27,7 @@ jobs: run: | bun install bun clean - bun lint -- --reporter=github - - - name: Commit and Push Changes - uses: EndBug/add-and-commit@v9 - with: - add: "src" - message: "Linting" - committer_name: "GitHub Action" - committer_email: "action@github.com" + bun lint -- --reporter=junit > lint.xml - name: Start proxy run: | @@ -45,6 +37,12 @@ jobs: run: | bun test --reporter=junit --reporter-outfile=./bun.xml + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + if: success() || failure() + with: + report_paths: "*.xml" + - name: Run Docker Build run: | bun build:docker From 488e27fd1d1d36591da6c840b94295fa61442362 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:35:41 +0200 Subject: [PATCH 247/369] CI/CD: Fix linter --- .github/workflows/pipeline.yaml | 8 +++++--- .gitignore | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 4ae1baea..f0f3d613 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -8,7 +8,9 @@ on: permissions: contents: write - packages: write + checks: write + id-token: write + pull-requests: write jobs: test: @@ -27,7 +29,7 @@ jobs: run: | bun install bun clean - bun lint -- --reporter=junit > lint.xml + bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src --reporter=junit > lint.xml - name: Start proxy run: | @@ -35,7 +37,7 @@ jobs: - name: Run Unit-tests run: | - bun test --reporter=junit --reporter-outfile=./bun.xml + bun test --reporter=junit --reporter-outfile=./unit-test.xml - name: Publish Test Report uses: mikepenz/action-junit-report@v5 diff --git a/.gitignore b/.gitignore index 527c7b3f..941ca3d9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ .test dependency-graph* build -data \ No newline at end of file +data +*.xml \ No newline at end of file From 2570cb881a7ea5b977a41e1bbae9c275eea402ce Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:37:46 +0200 Subject: [PATCH 248/369] CI/CD: Logging to debug --- .github/workflows/pipeline.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index f0f3d613..f77350bf 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -38,6 +38,7 @@ jobs: - name: Run Unit-tests run: | bun test --reporter=junit --reporter-outfile=./unit-test.xml + ls -lah - name: Publish Test Report uses: mikepenz/action-junit-report@v5 From fe46d26ee39ed17a9eccacfcb120bde3aa4e4f81 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:44:21 +0200 Subject: [PATCH 249/369] CI/CD: This seems stupid --- .github/workflows/pipeline.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index f77350bf..7f8f09d6 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -31,6 +31,12 @@ jobs: bun clean bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src --reporter=junit > lint.xml + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + if: success() || failure() + with: + report_paths: "*.xml" + - name: Start proxy run: | docker compose -f docker/docker-compose.dev.yaml up -d From 7846fedb3bcd80c6c368cf99ea816bd2693d703b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:46:14 +0200 Subject: [PATCH 250/369] CI/CD: Maybe this? --- .github/workflows/pipeline.yaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 7f8f09d6..56e25f38 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -31,12 +31,6 @@ jobs: bun clean bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src --reporter=junit > lint.xml - - name: Publish Test Report - uses: mikepenz/action-junit-report@v5 - if: success() || failure() - with: - report_paths: "*.xml" - - name: Start proxy run: | docker compose -f docker/docker-compose.dev.yaml up -d @@ -50,7 +44,7 @@ jobs: uses: mikepenz/action-junit-report@v5 if: success() || failure() with: - report_paths: "*.xml" + report_paths: "*/**/*.xml" - name: Run Docker Build run: | From e17d6b8fb9e7ed413afb50c57707127b6483cc24 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:47:38 +0200 Subject: [PATCH 251/369] CI/CD: Yes? No? --- .github/workflows/pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 56e25f38..fbfecf3a 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -29,7 +29,7 @@ jobs: run: | bun install bun clean - bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src --reporter=junit > lint.xml + bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src --reporter=junit > lint-test.xml - name: Start proxy run: | From 69cc950ace8c2026a30a8879210d149f1daec563 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:51:06 +0200 Subject: [PATCH 252/369] CI/CD: This might be it --- .github/workflows/pipeline.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index fbfecf3a..d3bb20bc 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -45,6 +45,8 @@ jobs: if: success() || failure() with: report_paths: "*/**/*.xml" + include_passed: true + detailed_summary: true - name: Run Docker Build run: | From 18e41a24726d20180e5ae76b59cb8956b8e8654b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:54:19 +0200 Subject: [PATCH 253/369] CI/CD: Remove detailed summarrry --- .github/workflows/pipeline.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index d3bb20bc..725692cb 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -46,7 +46,6 @@ jobs: with: report_paths: "*/**/*.xml" include_passed: true - detailed_summary: true - name: Run Docker Build run: | From e9e37a1e724fe7e3f4220de9fc614af40a1a6ba0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 22:55:30 +0200 Subject: [PATCH 254/369] CI/CD: Fix path --- .github/workflows/pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 725692cb..9ec0b463 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -44,7 +44,7 @@ jobs: uses: mikepenz/action-junit-report@v5 if: success() || failure() with: - report_paths: "*/**/*.xml" + report_paths: "**/*.xml" include_passed: true - name: Run Docker Build From 8fe2e717e58e4fd385b49e292ba6c230e06a7675 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 23:14:16 +0200 Subject: [PATCH 255/369] CI/CD: Fix reports --- .github/workflows/pipeline.yaml | 18 ++++++++++++---- .gitignore | 3 ++- src/core/database/database.ts | 38 ++++++++++++++++----------------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 9ec0b463..8df21a0f 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -25,11 +25,21 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Knip + run: | + bun knip -- --reporter markdown > knip.report.md + - name: Lint run: | bun install bun clean - bun biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src --reporter=junit > lint-test.xml + bun biome check \ + --formatter-enabled=true \ + --linter-enabled=true \ + --organize-imports-enabled=true \ + --fix \ + --reporter=junit \ + src > lint-test.xml - name: Start proxy run: | @@ -42,10 +52,10 @@ jobs: - name: Publish Test Report uses: mikepenz/action-junit-report@v5 - if: success() || failure() with: - report_paths: "**/*.xml" - include_passed: true + report_paths: | + lint-test.xml + unit-test.xml - name: Run Docker Build run: | diff --git a/.gitignore b/.gitignore index 941ca3d9..bb2174e8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ dependency-graph* build data -*.xml \ No newline at end of file +*.xml +*.report.md \ No newline at end of file diff --git a/src/core/database/database.ts b/src/core/database/database.ts index db33ec9c..d6a0be12 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -4,16 +4,16 @@ import { existsSync, mkdirSync } from "node:fs"; const dataFolder = "data"; if (!existsSync(dataFolder)) { - mkdirSync(dataFolder, { recursive: true }); + mkdirSync(dataFolder, { recursive: true }); } -export const databasePath = "data/dockstatapi.db"; +const databasePath = "data/dockstatapi.db"; export const db = new Database(databasePath, { strict: true }); db.exec("PRAGMA journal_mode = WAL;"); export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -77,25 +77,25 @@ export function init() { ); `); - const configRow = db - .prepare("SELECT COUNT(*) AS count FROM config") - .get() as { count: number }; + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; - if (configRow.count === 0) { - db.prepare( - 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")', - ).run(); - } + if (configRow.count === 0) { + db.prepare( + 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")' + ).run(); + } - const hostRow = db - .prepare("SELECT COUNT(*) AS count FROM docker_hosts") - .get() as { count: number }; + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; - if (hostRow.count === 0) { - db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", - ).run("Localhost", "localhost:2375", false); - } + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" + ).run("Localhost", "localhost:2375", false); + } } init(); From 7936bb2e9c914d2be97863bdc5f0193fd6611aae Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 23:15:06 +0200 Subject: [PATCH 256/369] CI/CD: Fix readd package permission --- .github/workflows/pipeline.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 8df21a0f..7eefb5c6 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -11,6 +11,7 @@ permissions: checks: write id-token: write pull-requests: write + packages: write jobs: test: From 39777b551808b32b76cf7814eb8d13fd19340bc8 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 23:19:42 +0200 Subject: [PATCH 257/369] CI//CD: Add knip report --- .github/workflows/pipeline.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 7eefb5c6..44c6a3a7 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -26,10 +26,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Knip - run: | - bun knip -- --reporter markdown > knip.report.md - - name: Lint run: | bun install @@ -41,6 +37,11 @@ jobs: --fix \ --reporter=junit \ src > lint-test.xml + bun knip -- --reporter markdown > knip.report.md + + - name: Read Knip report + id: getknip + run: echo "::set-output name=content::$(cat knip.report.md)" - name: Start proxy run: | @@ -57,6 +58,7 @@ jobs: report_paths: | lint-test.xml unit-test.xml + summary: ${{ steps.getknip.outputs.content }} - name: Run Docker Build run: | From 08772861d0116ceb0b08fc7ab02fbe1b08164ddc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 23:20:36 +0200 Subject: [PATCH 258/369] CI/CD: Fix add include_passed --- .github/workflows/pipeline.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 44c6a3a7..bdb1cead 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -55,6 +55,7 @@ jobs: - name: Publish Test Report uses: mikepenz/action-junit-report@v5 with: + include_passed: true report_paths: | lint-test.xml unit-test.xml From cc0bf5ea7e1970ce6fe3237d1089a54b7b97a613 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 16 Apr 2025 23:24:32 +0200 Subject: [PATCH 259/369] CI/CD: Change to GH output --- .github/workflows/pipeline.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index bdb1cead..fcb55d25 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -15,6 +15,8 @@ permissions: jobs: test: + outputs: + knip: ${{ steps.getknip.outputs.test }} name: Test Build on Push runs-on: ubuntu-latest steps: @@ -41,7 +43,7 @@ jobs: - name: Read Knip report id: getknip - run: echo "::set-output name=content::$(cat knip.report.md)" + run: echo "{content}={$(cat knip.report.md)}" >> $GITHUB_OUTPUT - name: Start proxy run: | From 00f2d80794159d6064cacf3315ee025c058d1dd9 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 17 Apr 2025 16:56:57 +0200 Subject: [PATCH 260/369] CI/CD: FIx Reporting --- .github/CODEOWNERS | 1 + .github/workflows/pipeline.yaml | 18 +++++++++-------- .gitignore | 4 ++-- src/core/database/database.ts | 36 ++++++++++++++++----------------- 4 files changed, 31 insertions(+), 28 deletions(-) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..7eeacf33 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +*.ts info@itsnik.de \ No newline at end of file diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index fcb55d25..2d15b2f9 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -15,8 +15,6 @@ permissions: jobs: test: - outputs: - knip: ${{ steps.getknip.outputs.test }} name: Test Build on Push runs-on: ubuntu-latest steps: @@ -39,11 +37,17 @@ jobs: --fix \ --reporter=junit \ src > lint-test.xml - bun knip -- --reporter markdown > knip.report.md + bun knip --reporter markdown > Knip-Report.md - - name: Read Knip report - id: getknip - run: echo "{content}={$(cat knip.report.md)}" >> $GITHUB_OUTPUT + - name: Create or update PR comment with Knip report + uses: peter-evans/create-or-update-comment@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + repository: ${{ github.repository }} + issue-number: ${{ github.event.pull_request.number }} + body-file: Knip-Report.md + comment-id: knip-report + edit-mode: replace - name: Start proxy run: | @@ -59,9 +63,7 @@ jobs: with: include_passed: true report_paths: | - lint-test.xml unit-test.xml - summary: ${{ steps.getknip.outputs.content }} - name: Run Docker Build run: | diff --git a/.gitignore b/.gitignore index bb2174e8..569df45a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,8 @@ /stacks /node_modules .test -dependency-graph* build data *.xml -*.report.md \ No newline at end of file +*.report.md +dependency-* \ No newline at end of file diff --git a/src/core/database/database.ts b/src/core/database/database.ts index d6a0be12..a8d4c13e 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -4,7 +4,7 @@ import { existsSync, mkdirSync } from "node:fs"; const dataFolder = "data"; if (!existsSync(dataFolder)) { - mkdirSync(dataFolder, { recursive: true }); + mkdirSync(dataFolder, { recursive: true }); } const databasePath = "data/dockstatapi.db"; @@ -13,7 +13,7 @@ export const db = new Database(databasePath, { strict: true }); db.exec("PRAGMA journal_mode = WAL;"); export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -77,25 +77,25 @@ export function init() { ); `); - const configRow = db - .prepare("SELECT COUNT(*) AS count FROM config") - .get() as { count: number }; + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; - if (configRow.count === 0) { - db.prepare( - 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")' - ).run(); - } + if (configRow.count === 0) { + db.prepare( + 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")', + ).run(); + } - const hostRow = db - .prepare("SELECT COUNT(*) AS count FROM docker_hosts") - .get() as { count: number }; + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; - if (hostRow.count === 0) { - db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" - ).run("Localhost", "localhost:2375", false); - } + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ).run("Localhost", "localhost:2375", false); + } } init(); From deb505f117b399a445fa46efe2c6e9e1226dd1bd Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 17 Apr 2025 17:02:30 +0200 Subject: [PATCH 261/369] CI/CD: Fix pathing --- .github/workflows/pipeline.yaml | 10 +++++++++- .gitignore | 4 ++-- knip.report.md | Bin 0 -> 36 bytes 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 knip.report.md diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 2d15b2f9..4d179bbf 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -39,6 +39,14 @@ jobs: src > lint-test.xml bun knip --reporter markdown > Knip-Report.md + - name: Commit and Push Changes + uses: EndBug/add-and-commit@v9 + with: + add: "src/" + message: "Update dependency graphs" + committer_name: "GitHub Action" + committer_email: "action@github.com" + - name: Create or update PR comment with Knip report uses: peter-evans/create-or-update-comment@v3 with: @@ -46,7 +54,7 @@ jobs: repository: ${{ github.repository }} issue-number: ${{ github.event.pull_request.number }} body-file: Knip-Report.md - comment-id: knip-report + comment-id: knip-report.md edit-mode: replace - name: Start proxy diff --git a/.gitignore b/.gitignore index 569df45a..1c7d1e18 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,5 @@ build data *.xml -*.report.md -dependency-* \ No newline at end of file +dependency-* +Knip-Report.md \ No newline at end of file diff --git a/knip.report.md b/knip.report.md new file mode 100644 index 0000000000000000000000000000000000000000..883973e8cd66b2063469f101056c1e31272ec606 GIT binary patch literal 36 jcmezWPnki1!J8qEA(Np1$SPt;1=9IIx`ct3feVZQqx=TF literal 0 HcmV?d00001 From 5cbe174829079015740c5fa57afbdd4f1ecceca6 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:09:38 +0200 Subject: [PATCH 262/369] CI/CD: Update pipeline.yaml --- .github/workflows/pipeline.yaml | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 4d179bbf..70be6497 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -126,3 +126,37 @@ jobs: push: true platforms: linux/amd64,linux/arm64 tags: ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }} + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: 'ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }}' + format: 'sarif' + output: 'trivy-results.sarif' + exit-code: '1' + ignore-unfixed: true + vuln-type: 'os,library' + severity: "MEDIUM,HIGH,CRITICAL" + + - name: Scan image in a private registry + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: "ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }}" + scan-type: image + format: 'github' + output: 'dependency-results.sbom.json' + github-pat: ${{ secrets.GITHUB_TOKEN }} + severity: "MEDIUM,HIGH,CRITICAL" + scanners: "vuln" + + - name: Upload trivy report as a Github artifact + uses: actions/upload-artifact@v4 + with: + name: trivy-sbom-report + path: '${{ github.workspace }}/dependency-results.sbom.json' + retention-days: 20 + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' From 718ddd4a66f5d64e380e01cbaf186fbe23358614 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:16:42 +0200 Subject: [PATCH 263/369] CI/CD: Update pipeline.yaml --- .github/workflows/pipeline.yaml | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 70be6497..8897c877 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -30,33 +30,17 @@ jobs: run: | bun install bun clean - bun biome check \ - --formatter-enabled=true \ - --linter-enabled=true \ - --organize-imports-enabled=true \ - --fix \ - --reporter=junit \ - src > lint-test.xml - bun knip --reporter markdown > Knip-Report.md + bun biome ci + bun knip --reporter markdown > .github/Knip-Report.md - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: - add: "src/" - message: "Update dependency graphs" + add: '["src/",".github/*.md"]' + message: "Update dependency graphs and upload Knip-report.md" committer_name: "GitHub Action" committer_email: "action@github.com" - - name: Create or update PR comment with Knip report - uses: peter-evans/create-or-update-comment@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - repository: ${{ github.repository }} - issue-number: ${{ github.event.pull_request.number }} - body-file: Knip-Report.md - comment-id: knip-report.md - edit-mode: replace - - name: Start proxy run: | docker compose -f docker/docker-compose.dev.yaml up -d From 252eaae49864bdb21ebf42bb6ef8c0f0d21772f3 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:20:47 +0200 Subject: [PATCH 264/369] CI/CD: pipeline.yaml --- .github/workflows/pipeline.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 8897c877..efa92539 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -116,12 +116,17 @@ jobs: with: image-ref: 'ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }}' format: 'sarif' - output: 'trivy-results.sarif' + output: 'trivy-results-${{ env.IMAGE_TAG }}.sarif' exit-code: '1' ignore-unfixed: true vuln-type: 'os,library' severity: "MEDIUM,HIGH,CRITICAL" + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results-${{ env.IMAGE_TAG }}.sarif' + - name: Scan image in a private registry uses: aquasecurity/trivy-action@0.28.0 with: @@ -139,8 +144,3 @@ jobs: name: trivy-sbom-report path: '${{ github.workspace }}/dependency-results.sbom.json' retention-days: 20 - - - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: 'trivy-results.sarif' From 5ab15c7e42f6732a0fd423f9464b9c6283e9de57 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:23:25 +0200 Subject: [PATCH 265/369] CI/CD: Update pipeline.yaml --- .github/workflows/pipeline.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index efa92539..7bd54579 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -12,6 +12,7 @@ permissions: id-token: write pull-requests: write packages: write + security-events: write jobs: test: @@ -127,7 +128,7 @@ jobs: with: sarif_file: 'trivy-results-${{ env.IMAGE_TAG }}.sarif' - - name: Scan image in a private registry + - name: Scan image dependencies uses: aquasecurity/trivy-action@0.28.0 with: image-ref: "ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }}" From 022883268347d2e1b23891ef0994fb17aaeb9400 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:29:43 +0200 Subject: [PATCH 266/369] Update pipeline.yaml --- .github/workflows/pipeline.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 7bd54579..432f7deb 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -32,7 +32,7 @@ jobs: bun install bun clean bun biome ci - bun knip --reporter markdown > .github/Knip-Report.md + bun knip --reporter markdown -- > .github/Knip-Report.md - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 From 8f4b05c2ba6ad1d2cd5f1e1bcd08464815094bb7 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:36:07 +0200 Subject: [PATCH 267/369] CI/CD: Update pipeline.yaml --- .github/workflows/pipeline.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 432f7deb..97817ee2 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -32,12 +32,15 @@ jobs: bun install bun clean bun biome ci - bun knip --reporter markdown -- > .github/Knip-Report.md + bun knip --reporter markdown > .github/Knip-Report.md + + - name: "Debug: list .github" + run: ls -l .github - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: - add: '["src/",".github/*.md"]' + add: '["src/",".github/Knip-Report.md"]' message: "Update dependency graphs and upload Knip-report.md" committer_name: "GitHub Action" committer_email: "action@github.com" From b343c3c108fc0572aa313705fac579e5ff1f8ad0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:39:40 +0200 Subject: [PATCH 268/369] CI/CD: Update pipeline.yaml --- .github/workflows/pipeline.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 97817ee2..94c570d7 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -34,12 +34,10 @@ jobs: bun biome ci bun knip --reporter markdown > .github/Knip-Report.md - - name: "Debug: list .github" - run: ls -l .github - - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: + commit: "-f" add: '["src/",".github/Knip-Report.md"]' message: "Update dependency graphs and upload Knip-report.md" committer_name: "GitHub Action" From 6c2b5b03dbd0977552ff7758f398dfde1bc037c3 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:41:15 +0200 Subject: [PATCH 269/369] Update pipeline.yaml --- .github/workflows/pipeline.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 94c570d7..9804f5c2 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -32,13 +32,13 @@ jobs: bun install bun clean bun biome ci - bun knip --reporter markdown > .github/Knip-Report.md + bun knip --reporter markdown > Knip-Report.md - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: commit: "-f" - add: '["src/",".github/Knip-Report.md"]' + add: '["src/","Knip-Report.md"]' message: "Update dependency graphs and upload Knip-report.md" committer_name: "GitHub Action" committer_email: "action@github.com" From 2ab6005570b58261f452b0263a7945b8a3bcb548 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:47:21 +0200 Subject: [PATCH 270/369] CI/CD: Update pipeline.yaml --- .github/workflows/pipeline.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 9804f5c2..46c8c551 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -13,7 +13,8 @@ permissions: pull-requests: write packages: write security-events: write - + issues: write + jobs: test: name: Test Build on Push @@ -32,7 +33,9 @@ jobs: bun install bun clean bun biome ci - bun knip --reporter markdown > Knip-Report.md + + - name: Post the knip results + uses: codex-/knip-reporter@v2 - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 From 3755750ab46c9cd5a3fa58e8c0e16fcf4d1932d5 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 25 Apr 2025 09:49:20 +0200 Subject: [PATCH 271/369] CI/CD: Update pipeline.yaml --- .github/workflows/pipeline.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 46c8c551..325e0211 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -35,14 +35,15 @@ jobs: bun biome ci - name: Post the knip results + if: ${{ github.event_name == 'pull_request' }} uses: codex-/knip-reporter@v2 - name: Commit and Push Changes uses: EndBug/add-and-commit@v9 with: commit: "-f" - add: '["src/","Knip-Report.md"]' - message: "Update dependency graphs and upload Knip-report.md" + add: "src/" + message: "Update dependency graphs" committer_name: "GitHub Action" committer_email: "action@github.com" From 5ae009e91826f356dc21fecb32b155cff6ddee3d Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 29 Apr 2025 12:08:07 +0200 Subject: [PATCH 272/369] Create ci.yml --- .github/workflows/ci.yml | 77 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..b6cae8d0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,77 @@ +name: Continuous Integration + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + checks: write + security-events: write + +jobs: + lint-test: + name: Lint and Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run linter + run: bun run biome ci + + - name: Run unit tests + run: bun test --reporter=junit --reporter-outfile=./unit-test.xml + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + with: + report_paths: 'unit-test.xml' + + build-scan: + name: Build and Security Scan + runs-on: ubuntu-latest + needs: lint-test + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile + tags: dockstatapi:ci-${{ github.sha }} + load: true + + - name: Start and test container + run: | + docker run --name test-container -d dockstatapi:ci-${{ github.sha }} + sleep 10 + docker ps | grep test-container + docker logs test-container + docker stop test-container + + - name: Trivy vulnerability scan + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: 'dockstatapi:ci-${{ github.sha }}' + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'HIGH,CRITICAL' + + - name: Upload security results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' From f90dc15ffea705cf1a21ebe183a645d4ac20c6dc Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 29 Apr 2025 12:08:49 +0200 Subject: [PATCH 273/369] Create cd.yml --- .github/workflows/cd.yml | 64 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/cd.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000..15f58c44 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,64 @@ +name: Continuous Delivery + +on: + release: + types: [published, prereleased] + +permissions: + contents: read + packages: write + +jobs: + publish: + name: Publish Container Image + runs-on: ubuntu-latest + environment: production + steps: + - uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Determine tags + id: tags + uses: docker/metadata-action@v5 + with: + images: ghcr.io/its4nik/dockstatapi + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.tags.outputs.tags }} + + sbom: + name: Generate SBOM + runs-on: ubuntu-latest + needs: publish + steps: + - name: Generate SBOM + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ghcr.io/its4nik/dockstatapi:${{ github.event.release.tag_name }} + format: spdx-json + output: sbom.json + + - name: Upload SBOM + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: sbom.json From d95032f7a617399f00c2f4f77f0374feba339216 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 29 Apr 2025 12:09:29 +0200 Subject: [PATCH 274/369] Delete .github/workflows/pipeline.yaml --- .github/workflows/pipeline.yaml | 152 -------------------------------- 1 file changed, 152 deletions(-) delete mode 100644 .github/workflows/pipeline.yaml diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml deleted file mode 100644 index 325e0211..00000000 --- a/.github/workflows/pipeline.yaml +++ /dev/null @@ -1,152 +0,0 @@ -name: Docker Build and Test Workflow - -on: - push: - branches: ["**"] - release: - types: [published, prereleased] - -permissions: - contents: write - checks: write - id-token: write - pull-requests: write - packages: write - security-events: write - issues: write - -jobs: - test: - name: Test Build on Push - runs-on: ubuntu-latest - steps: - - uses: oven-sh/setup-bun@v2 - name: Setup Bun - with: - bun-version: latest - - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Lint - run: | - bun install - bun clean - bun biome ci - - - name: Post the knip results - if: ${{ github.event_name == 'pull_request' }} - uses: codex-/knip-reporter@v2 - - - name: Commit and Push Changes - uses: EndBug/add-and-commit@v9 - with: - commit: "-f" - add: "src/" - message: "Update dependency graphs" - committer_name: "GitHub Action" - committer_email: "action@github.com" - - - name: Start proxy - run: | - docker compose -f docker/docker-compose.dev.yaml up -d - - - name: Run Unit-tests - run: | - bun test --reporter=junit --reporter-outfile=./unit-test.xml - ls -lah - - - name: Publish Test Report - uses: mikepenz/action-junit-report@v5 - with: - include_passed: true - report_paths: | - unit-test.xml - - - name: Run Docker Build - run: | - bun build:docker - - - name: Start Docker container and check uptime - run: | - docker run --name dockstatapi --rm -d dockstatapi:local - sleep 30 - if docker ps --filter "name=dockstatapi" --filter "status=running" | grep dockstatapi; then - docker kill dockstatapi - exit 0 - else - exit 1 - fi - - release: - name: Build and Push Docker Image on Release - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Determine image tag - id: tag - run: | - TAG=${GITHUB_REF##*/} - if [ "${{ github.event.release.prerelease }}" = "true" ]; then - TAG="${TAG}-rc" - fi - echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV - echo "Using tag: $TAG" - - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: . - file: docker/Dockerfile - push: true - platforms: linux/amd64,linux/arm64 - tags: ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }} - - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@0.28.0 - with: - image-ref: 'ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }}' - format: 'sarif' - output: 'trivy-results-${{ env.IMAGE_TAG }}.sarif' - exit-code: '1' - ignore-unfixed: true - vuln-type: 'os,library' - severity: "MEDIUM,HIGH,CRITICAL" - - - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: 'trivy-results-${{ env.IMAGE_TAG }}.sarif' - - - name: Scan image dependencies - uses: aquasecurity/trivy-action@0.28.0 - with: - image-ref: "ghcr.io/its4nik/dockstatapi:${{ env.IMAGE_TAG }}" - scan-type: image - format: 'github' - output: 'dependency-results.sbom.json' - github-pat: ${{ secrets.GITHUB_TOKEN }} - severity: "MEDIUM,HIGH,CRITICAL" - scanners: "vuln" - - - name: Upload trivy report as a Github artifact - uses: actions/upload-artifact@v4 - with: - name: trivy-sbom-report - path: '${{ github.workspace }}/dependency-results.sbom.json' - retention-days: 20 From 8a9e4f20cf5c1c8853548f30666f1105c68adaf1 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 29 Apr 2025 12:15:21 +0200 Subject: [PATCH 275/369] Update ci.yml --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6cae8d0..36bb85ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: Continuous Integration on: push: - branches: [main] + branches: ["**"] pull_request: - branches: [main] + branches: ["**"] permissions: contents: read From 5934b6c31bcbd761a1117a7ae9e27b05375cad1b Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 29 Apr 2025 13:25:29 +0200 Subject: [PATCH 276/369] Update ci.yml --- .github/workflows/ci.yml | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36bb85ae..501b9948 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,17 +6,18 @@ on: pull_request: branches: ["**"] -permissions: - contents: read - checks: write - security-events: write - jobs: lint-test: name: Lint and Test runs-on: ubuntu-latest + permissions: + contents: write + checks: write + security-events: write steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Bun uses: oven-sh/setup-bun@v2 @@ -29,6 +30,14 @@ jobs: - name: Run linter run: bun run biome ci + - name: Add linted files + run: git add src/ + + - name: Check for changes + id: check-changes + run: | + git diff --cached --quiet || echo "changes_detected=true" >> $GITHUB_OUTPUT + - name: Run unit tests run: bun test --reporter=junit --reporter-outfile=./unit-test.xml @@ -37,10 +46,24 @@ jobs: with: report_paths: 'unit-test.xml' + - name: Commit and push lint changes + if: | + steps.check-changes.outputs.changes_detected == 'true' && + github.event_name == 'push' + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + git commit -m "chore: apply lint fixes [skip ci]" + git push + build-scan: name: Build and Security Scan runs-on: ubuntu-latest needs: lint-test + permissions: + contents: read + checks: write + security-events: write steps: - uses: actions/checkout@v4 From fdd6ba8eaf0d192dd66323a1d966ef145b8329ab Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 29 Apr 2025 14:07:30 +0200 Subject: [PATCH 277/369] CI/CD: Fix test pipeline --- .github/workflows/ci.yml | 8 ++++++-- biome.json | 2 +- src/index.ts | 2 +- src/tests/helper.ts | 5 ++++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 501b9948..d8299b50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - + - name: Setup Bun uses: oven-sh/setup-bun@v2 with: @@ -39,7 +39,11 @@ jobs: git diff --cached --quiet || echo "changes_detected=true" >> $GITHUB_OUTPUT - name: Run unit tests - run: bun test --reporter=junit --reporter-outfile=./unit-test.xml + run: | + export DOCKSTATAPI_PORT=5971 + bun clean + bun test --reporter=junit --reporter-outfile=./unit-test.xml + bun clean - name: Publish Test Report uses: mikepenz/action-junit-report@v5 diff --git a/biome.json b/biome.json index a02c1a30..bdb5eccf 100644 --- a/biome.json +++ b/biome.json @@ -3,7 +3,7 @@ "vcs": { "enabled": true, "clientKind": "git", - "useIgnoreFile": false + "useIgnoreFile": true }, "formatter": { "enabled": true, diff --git a/src/index.ts b/src/index.ts index 8090d9a4..0bc6a37a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,7 +145,7 @@ async function startServer() { } try { - DockStatAPI.listen(3000, ({ hostname, port }) => { + DockStatAPI.listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { console.log("----- [ ############## ]"); logger.info(`DockStatAPI is running at http://${hostname}:${port}`); logger.info( diff --git a/src/tests/helper.ts b/src/tests/helper.ts index fabc45b3..6e816b4b 100644 --- a/src/tests/helper.ts +++ b/src/tests/helper.ts @@ -5,7 +5,10 @@ import { logger } from "~/core/utils/logger"; import { DockStatAPI } from ".."; export const API_KEY = "TestKey"; -const server = "http://localhost:3001"; + +const host = "http://localhost"; +const port = process.env.DOCKSTATAPI_PORT || 3000; +const server = `${host}:${port}` export async function runTestResponse( path: string, From dc7e7a503c5a5ab7445ca843adc4e3d5638e2d8c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 29 Apr 2025 14:09:53 +0200 Subject: [PATCH 278/369] CI/CD: Fix Linter --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8299b50..8e154656 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: run: bun install - name: Run linter - run: bun run biome ci + run: bun run biome lint --fix - name: Add linted files run: git add src/ From df40a845f4e062b42bda849693fbe91f83351811 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 29 Apr 2025 14:12:36 +0200 Subject: [PATCH 279/369] CI/CD: Disable padding in github logs --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e154656..fe6155ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,9 @@ jobs: run: bun install - name: Run linter - run: bun run biome lint --fix + run: | + export PAD_NEW_LINES=false + bun run biome lint --fix - name: Add linted files run: git add src/ From 76ad637dca3b724a081a285870ac05ea27927f3e Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 29 Apr 2025 14:13:41 +0200 Subject: [PATCH 280/369] CI/CD: Disable padding in github logs --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe6155ae..e07e63ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,6 @@ jobs: - name: Run linter run: | - export PAD_NEW_LINES=false bun run biome lint --fix - name: Add linted files @@ -43,6 +42,7 @@ jobs: - name: Run unit tests run: | export DOCKSTATAPI_PORT=5971 + export PAD_NEW_LINES=false bun clean bun test --reporter=junit --reporter-outfile=./unit-test.xml bun clean From f2b58f26827daf0e29a823c252232b498cbf2bfa Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 29 Apr 2025 14:23:46 +0200 Subject: [PATCH 281/369] CI/CD: Forgot to activate the socket proxy in ci --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e07e63ae..0dbe22b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,10 @@ jobs: - name: Install dependencies run: bun install + - name: Knip check + if: ${{ github.event_name == 'pull_request' }} + uses: codex-/knip-reporter@v2 + - name: Run linter run: | bun run biome lint --fix @@ -43,6 +47,7 @@ jobs: run: | export DOCKSTATAPI_PORT=5971 export PAD_NEW_LINES=false + docker compose -f docker/docker-compose.dev.yaml up -d bun clean bun test --reporter=junit --reporter-outfile=./unit-test.xml bun clean From 566837751e19959545132cb6753f5fd1334107e4 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 29 Apr 2025 14:38:51 +0200 Subject: [PATCH 282/369] Update ci.yml --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dbe22b9..c0d2b81f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,10 @@ jobs: - name: Run linter run: | - bun run biome lint --fix + bun biome format --fix + bun biome lint --fix + bun biome check --fix + bun biome ci --fix - name: Add linted files run: git add src/ From 30618fbebbe507c746c455d863a67be303245776 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 29 Apr 2025 14:45:17 +0200 Subject: [PATCH 283/369] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0d2b81f..72929a70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: bun biome format --fix bun biome lint --fix bun biome check --fix - bun biome ci --fix + bun biome ci - name: Add linted files run: git add src/ From 205c1e4865c978a35410f3d42bf4eeee78c8afd9 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 29 Apr 2025 12:45:37 +0000 Subject: [PATCH 284/369] chore: apply lint fixes [skip ci] --- src/index.ts | 23 +++++++++++++---------- src/tests/helper.ts | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0bc6a37a..8d19f449 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,16 +145,19 @@ async function startServer() { } try { - DockStatAPI.listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }); + DockStatAPI.listen( + process.env.DOCKSTATAPI_PORT || 3000, + ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + }, + ); } catch (error) { logger.error("Failed to start server:", error); process.exit(1); diff --git a/src/tests/helper.ts b/src/tests/helper.ts index 6e816b4b..60161113 100644 --- a/src/tests/helper.ts +++ b/src/tests/helper.ts @@ -8,7 +8,7 @@ export const API_KEY = "TestKey"; const host = "http://localhost"; const port = process.env.DOCKSTATAPI_PORT || 3000; -const server = `${host}:${port}` +const server = `${host}:${port}`; export async function runTestResponse( path: string, From de68073f39f884d77d37f146db40090253fda312 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 30 Apr 2025 19:03:56 +0200 Subject: [PATCH 285/369] Feat: Update swagger api with better examples and added submodule for types --- .github/workflows/cd.yml | 1 - .github/workflows/ci.yml | 14 +- .gitmodules | 3 + package.json | 9 +- src/core/database/logs.ts | 32 ++- src/core/utils/logger.ts | 31 +- src/index.ts | 4 +- src/routes/api-config.ts | 353 ++++++++++++++++++++++- src/routes/docker-manager.ts | 151 ++++++++++ src/routes/docker-stats.ts | 159 +++++++++++ src/routes/live-logs.ts | 2 +- src/routes/logs.ts | 166 +++++++++++ src/routes/stacks.ts | 320 +++++++++++++++++++++ src/routes/utils.ts | 75 +++++ src/typings | 1 + src/typings/database.ts | 27 -- src/typings/docker-compose.ts | 522 ---------------------------------- src/typings/docker.ts | 41 --- src/typings/dockerode.ts | 162 ----------- src/typings/elysiajs.ts | 12 - src/typings/misc.ts | 5 - src/typings/plugin.ts | 26 -- src/typings/websocket.ts | 15 - 23 files changed, 1278 insertions(+), 853 deletions(-) create mode 100644 .gitmodules create mode 160000 src/typings delete mode 100644 src/typings/database.ts delete mode 100644 src/typings/docker-compose.ts delete mode 100644 src/typings/docker.ts delete mode 100644 src/typings/dockerode.ts delete mode 100644 src/typings/elysiajs.ts delete mode 100644 src/typings/misc.ts delete mode 100644 src/typings/plugin.ts delete mode 100644 src/typings/websocket.ts diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 15f58c44..1b60b66a 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -12,7 +12,6 @@ jobs: publish: name: Publish Container Image runs-on: ubuntu-latest - environment: production steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72929a70..f1c83b24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: - name: Publish Test Report uses: mikepenz/action-junit-report@v5 with: - report_paths: 'unit-test.xml' + report_paths: "unit-test.xml" - name: Commit and push lint changes if: | @@ -67,7 +67,7 @@ jobs: run: | git config --global user.name "GitHub Actions" git config --global user.email "actions@github.com" - git commit -m "chore: apply lint fixes [skip ci]" + git commit -m "CQL: Apply lint fixes [skip ci]" git push build-scan: @@ -103,12 +103,12 @@ jobs: - name: Trivy vulnerability scan uses: aquasecurity/trivy-action@0.28.0 with: - image-ref: 'dockstatapi:ci-${{ github.sha }}' - format: 'sarif' - output: 'trivy-results.sarif' - severity: 'HIGH,CRITICAL' + image-ref: "dockstatapi:ci-${{ github.sha }}" + format: "sarif" + output: "trivy-results.sarif" + severity: "HIGH,CRITICAL" - name: Upload security results uses: github/codeql-action/upload-sarif@v3 with: - sarif_file: 'trivy-results.sarif' + sarif_file: "trivy-results.sarif" diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..71322072 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/typings"] + path = src/typings + url = git@github.com:Its4Nik/dockstat-types.git diff --git a/package.json b/package.json index 8b64f69f..6bf8d3bc 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi*.db* && echo 'success'", "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi*.db* && echo 'success'", "knip": "knip", - "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src" + "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src", + "test": "bun test src/tests/**/*.test.ts" }, "dependencies": { "@elysiajs/server-timing": "^1.2.1", @@ -47,5 +48,7 @@ "wrap-ansi": "^9.0.0" }, "module": "src/index.js", - "trustedDependencies": ["protobufjs"] -} + "trustedDependencies": [ + "protobufjs" + ] +} \ No newline at end of file diff --git a/src/core/database/logs.ts b/src/core/database/logs.ts index 3fba8dcc..ad100c4d 100644 --- a/src/core/database/logs.ts +++ b/src/core/database/logs.ts @@ -16,6 +16,16 @@ const stmt = { deleteByLevel: db.prepare("DELETE FROM backend_log_entries WHERE level = ?"), }; +function convertToLogMessage(row: any): log_message { + return { + level: row.level, + timestamp: row.timestamp, + message: row.message, + file: row.file, + line: row.line, + }; +} + export function addLogEntry(data: log_message) { return executeDbOperation( "Add Log Entry", @@ -44,17 +54,17 @@ export function addLogEntry(data: log_message) { ); } -export function getAllLogs() { - return executeDbOperation("Get All Logs", () => stmt.selectAll.all()); +export function getAllLogs(): log_message[] { + return executeDbOperation( + "Get All Logs", + () => stmt.selectAll.all().map(convertToLogMessage), + ); } -export function getLogsByLevel(level: string) { +export function getLogsByLevel(level: string): log_message[] { return executeDbOperation( "Get Logs By Level", - () => stmt.selectByLevel.all(level), - () => { - if (typeof level !== "string") throw new TypeError("Invalid level type"); - }, + () => stmt.selectByLevel.all(level).map(convertToLogMessage), ); } @@ -63,11 +73,7 @@ export function clearAllLogs() { } export function clearLogsByLevel(level: string) { - return executeDbOperation( - "Clear Logs By Level", - () => stmt.deleteByLevel.run(level), - () => { - if (typeof level !== "string") throw new TypeError("Invalid level type"); - }, + return executeDbOperation("Clear Logs By Level", () => + stmt.deleteByLevel.run(level), ); } diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 53208767..30e60457 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -64,9 +64,21 @@ const levelColors: Record = { ut: chalk.hex("#9D00FF"), }; +const parseTimestamp = (timestamp: string): string => { + const [datePart, timePart] = timestamp.split(" "); + const [day, month] = datePart.split("/"); + const [hours, minutes, seconds] = timePart.split(":"); + const year = new Date().getFullYear(); + const date = new Date(year, parseInt(month) - 1, parseInt(day), parseInt(hours), parseInt(minutes), parseInt(seconds)); + return date.toISOString(); +}; + const handleWebSocketLog = (log: log_message) => { try { - logToClients(log); + logToClients({ + ...log, + timestamp: parseTimestamp(log.timestamp) + }); } catch (error) { console.error( `WebSocket logging failed: ${ @@ -81,7 +93,10 @@ const handleDatabaseLog = (log: log_message): void => { return; } try { - dbFunctions.addLogEntry(log); + dbFunctions.addLogEntry({ + ...log, + timestamp: parseTimestamp(log.timestamp) + }); } catch (error) { console.error( `Database logging failed: ${ @@ -160,17 +175,17 @@ export const logger = createLogger({ handleDatabaseLog({ level: processedLevel, - timestamp, + timestamp: timestamp, message: processedMessage, - file, - line, + file: file, + line: line, }); handleWebSocketLog({ level: processedLevel, - timestamp, + timestamp: timestamp, message: processedMessage, - file, - line, + file: file, + line: line, }); return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; diff --git a/src/index.ts b/src/index.ts index 8d19f449..062d8b88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,7 +76,7 @@ export const DockStatAPI = new Elysia() { name: "Utils", description: "Various utilities which might be useful", - }, + } ], }, }), @@ -84,7 +84,7 @@ export const DockStatAPI = new Elysia() .onBeforeHandle(async (context) => { const { path, request, set } = context; - if (path === "/health" || path.startsWith("/swagger")) { + if (path === "/health" || path.startsWith("/swagger") || path.startsWith("/trpc")) { logger.info(`Requested unguarded route: ${path}`); return; } diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 5be018ca..354ffb02 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -44,6 +44,48 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Returns current API configuration including data retention policies and security settings", + responses: { + "200": { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fetching_interval: { + type: "number", + example: 5 + }, + keep_data_for: { + type: "number", + example: 7 + }, + api_key: { + type: "string", + example: "hashed_api_key" + } + } + } + } + } + }, + "400": { + description: "Error retrieving configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting the DockStatAPI config" + } + } + } + } + } + } + } }, }, ) @@ -65,6 +107,51 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Lists all active plugins with their registration details and status", + responses: { + "200": { + description: "Successfully retrieved plugins", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-plugin" + }, + version: { + type: "string", + example: "1.0.0" + }, + status: { + type: "string", + example: "active" + } + } + } + } + } + } + }, + "400": { + description: "Error retrieving plugins", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting all registered plugins" + } + } + } + } + } + } + } }, }, ) @@ -89,16 +176,50 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) } }, { - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), detail: { tags: ["Management"], description: "Modifies core API settings including data collection intervals, retention periods, and security credentials", + responses: { + "200": { + description: "Successfully updated configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated DockStatAPI config" + } + } + } + } + } + }, + "400": { + description: "Error updating configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error updating the DockStatAPI config" + } + } + } + } + } + } + } }, + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), }, ) .get( @@ -130,6 +251,81 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) tags: ["Management"], description: "Displays package metadata including dependencies, contributors, and licensing information", + responses: { + "200": { + description: "Successfully retrieved package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0" + }, + description: { + type: "string", + example: "DockStatAPI is an API backend featuring plugins and more for DockStat" + }, + license: { + type: "string", + example: "CC BY-NC 4.0" + }, + authorName: { + type: "string", + example: "ItsNik" + }, + authorEmail: { + type: "string", + example: "info@itsnik.de" + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik" + }, + contributors: { + type: "array", + items: { + type: "string" + }, + example: [] + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0" + } + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38" + } + } + } + } + } + } + }, + "400": { + description: "Error retrieving package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error while reading package.json" + } + } + } + } + } + } + } }, }, ) @@ -147,6 +343,40 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) detail: { tags: ["Management"], description: "Backs up the internal database", + responses: { + "200": { + description: "Successfully created backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "backup_2024-03-20_12-00-00.db.bak" + } + } + } + } + } + }, + "400": { + description: "Error creating backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error backing up" + } + } + } + } + } + } + } }, }, ) @@ -177,6 +407,41 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) detail: { tags: ["Management"], description: "Lists all available backups", + responses: { + "200": { + description: "Successfully retrieved backup list", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "string" + }, + example: [ + "backup_2024-03-20_12-00-00.db.bak", + "backup_2024-03-19_12-00-00.db.bak" + ] + } + } + } + }, + "400": { + description: "Error retrieving backup list", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Reading Backup directory" + } + } + } + } + } + } + } }, }, ) @@ -205,14 +470,52 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) } }, { - query: t.Object({ - filename: t.Optional(t.String()), - }), detail: { tags: ["Management"], description: "Download a specific backup or the latest if no filename is provided", + responses: { + "200": { + description: "Successfully downloaded backup file", + content: { + "application/octet-stream": { + schema: { + type: "string", + format: "binary", + example: "Binary backup file content" + } + } + }, + headers: { + "Content-Disposition": { + schema: { + type: "string", + example: "attachment; filename=\"backup_2024-03-20_12-00-00.db.bak\"" + } + } + } + }, + "400": { + description: "Error downloading backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Backup download failed" + } + } + } + } + } + } + } }, + query: t.Object({ + filename: t.Optional(t.String()), + }), }, ) .post( @@ -252,6 +555,40 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) detail: { tags: ["Management"], description: "Restore database from uploaded backup file", + responses: { + "200": { + description: "Successfully restored database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Database restored successfully" + } + } + } + } + } + }, + "400": { + description: "Error restoring database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Database restoration error" + } + } + } + } + } + } + } }, }, ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 8caadd2f..13f33125 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -27,6 +27,40 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) tags: ["Management"], description: "Registers a new Docker host to the monitoring system with connection details", + responses: { + "200": { + description: "Successfully added Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Added docker host (Localhost)" + } + } + } + } + } + }, + "400": { + description: "Error adding Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error adding docker Host" + } + } + } + } + } + } + } }, body: t.Object({ name: t.String(), @@ -56,6 +90,40 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) tags: ["Management"], description: "Modifies existing Docker host configuration parameters (name, address, security)", + responses: { + "200": { + description: "Successfully updated Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated docker host (1)" + } + } + } + } + } + }, + "400": { + description: "Error updating Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to update host" + } + } + } + } + } + } + } }, body: t.Object({ id: t.Number(), @@ -87,6 +155,55 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) tags: ["Management"], description: "Lists all configured Docker hosts with their connection settings", + responses: { + "200": { + description: "Successfully retrieved Docker hosts", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "number", + example: 1 + }, + name: { + type: "string", + example: "Localhost" + }, + hostAddress: { + type: "string", + example: "localhost:2375" + }, + secure: { + type: "boolean", + example: false + } + } + } + } + } + } + }, + "400": { + description: "Error retrieving Docker hosts", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve hosts" + } + } + } + } + } + } + } }, }, ) @@ -111,6 +228,40 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) tags: ["Management"], description: "Removes Docker host from monitoring system and clears associated data", + responses: { + "200": { + description: "Successfully deleted Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Deleted docker host (1)" + } + } + } + } + } + }, + "400": { + description: "Error deleting Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to delete host" + } + } + } + } + } + } + } }, params: t.Object({ id: t.Number(), diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index d804afaf..db108161 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -108,6 +108,76 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) tags: ["Statistics"], description: "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", + responses: { + "200": { + description: "Successfully retrieved container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + containers: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + example: "abc123def456" + }, + hostId: { + type: "string", + example: "1" + }, + name: { + type: "string", + example: "example-container" + }, + image: { + type: "string", + example: "nginx:latest" + }, + status: { + type: "string", + example: "running" + }, + state: { + type: "string", + example: "running" + }, + cpuUsage: { + type: "number", + example: 0.5 + }, + memoryUsage: { + type: "number", + example: 1024 + } + } + } + } + } + } + } + } + }, + "400": { + description: "Error retrieving container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve containers" + } + } + } + } + } + } + } }, }, ) @@ -161,6 +231,95 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) tags: ["Statistics"], description: "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1 + }, + hostName: { + type: "string", + example: "Localhost" + }, + dockerVersion: { + type: "string", + example: "24.0.5" + }, + apiVersion: { + type: "string", + example: "1.41" + }, + os: { + type: "string", + example: "Linux" + }, + architecture: { + type: "string", + example: "x86_64" + }, + totalMemory: { + type: "number", + example: 16777216 + }, + totalCPU: { + type: "number", + example: 4 + }, + labels: { + type: "array", + items: { + type: "string" + }, + example: ["environment=production"] + }, + images: { + type: "number", + example: 10 + }, + containers: { + type: "number", + example: 5 + }, + containersPaused: { + type: "number", + example: 0 + }, + containersRunning: { + type: "number", + example: 4 + }, + containersStopped: { + type: "number", + example: 1 + } + } + } + } + } + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config" + } + } + } + } + } + } + } }, }, ); diff --git a/src/routes/live-logs.ts b/src/routes/live-logs.ts index 2e894b60..50e9ea84 100644 --- a/src/routes/live-logs.ts +++ b/src/routes/live-logs.ts @@ -11,7 +11,7 @@ const activeConnections = new Set>(); export const liveLogs = new Elysia({ prefix: "/logs" }).ws("/ws", { open(ws) { activeConnections.add(ws); - ws.send({ message: "Connection established" }); + ws.send({ message: "Connection established", level: "info", timestamp: new Date().toISOString(), file: "live-logs.ts", line: 14 }); logger.info(`New Logs WebSocket established (${ws.id})`); }, close(ws) { diff --git a/src/routes/logs.ts b/src/routes/logs.ts index 626e7230..20b3be2f 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -23,6 +23,55 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) tags: ["Management"], description: "Retrieves complete application log history from persistent storage", + responses: { + "200": { + description: "Successfully retrieved logs", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "number", + example: 1 + }, + level: { + type: "string", + example: "info" + }, + message: { + type: "string", + example: "Application started" + }, + timestamp: { + type: "string", + example: "2024-03-20T12:00:00Z" + } + } + } + } + } + } + }, + "500": { + description: "Error retrieving logs", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve logs" + } + } + } + } + } + } + } }, }, ) @@ -46,6 +95,55 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) tags: ["Management"], description: "Filters logs by severity level (debug, info, warn, error, fatal)", + responses: { + "200": { + description: "Successfully retrieved logs by level", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "number", + example: 1 + }, + level: { + type: "string", + example: "info" + }, + message: { + type: "string", + example: "Application started" + }, + timestamp: { + type: "string", + example: "2024-03-20T12:00:00Z" + } + } + } + } + } + } + }, + "500": { + description: "Error retrieving logs", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve logs" + } + } + } + } + } + } + } }, }, ) @@ -68,6 +166,40 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) detail: { tags: ["Management"], description: "Purges all historical log records from the database", + responses: { + "200": { + description: "Successfully cleared all logs", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { + type: "boolean", + example: true + } + } + } + } + } + }, + "500": { + description: "Error clearing logs", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Could not delete all logs" + } + } + } + } + } + } + } }, }, ) @@ -90,6 +222,40 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) detail: { tags: ["Management"], description: "Clears log entries matching specified severity level", + responses: { + "200": { + description: "Successfully cleared logs by level", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { + type: "boolean", + example: true + } + } + } + } + } + }, + "500": { + description: "Error clearing logs", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve logs" + } + } + } + } + } + } + } }, }, ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 56c8d1b6..52eaf860 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -68,6 +68,40 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Deploys a new Docker stack using a provided compose specification, allowing custom configurations and image updates", + responses: { + "200": { + description: "Successfully deployed stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack example-stack deployed successfully" + } + } + } + } + } + }, + "400": { + description: "Error deploying stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error deploying stack" + } + } + } + } + } + } + } }, body: t.Object({ compose_spec: t.Any(), @@ -105,6 +139,40 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Initiates a Docker stack, starting all associated containers", + responses: { + "200": { + description: "Successfully started stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 started successfully" + } + } + } + } + } + }, + "400": { + description: "Error starting stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error starting stack" + } + } + } + } + } + } + } }, body: t.Object({ stackId: t.Number(), @@ -135,6 +203,40 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Halts a running Docker stack and its containers while preserving configurations", + responses: { + "200": { + description: "Successfully stopped stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 stopped successfully" + } + } + } + } + } + }, + "400": { + description: "Error stopping stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error stopping stack" + } + } + } + } + } + } + } }, body: t.Object({ stackId: t.Number(), @@ -165,6 +267,40 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Performs full stack restart - stops and restarts all stack components in sequence", + responses: { + "200": { + description: "Successfully restarted stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 restarted successfully" + } + } + } + } + } + }, + "400": { + description: "Error restarting stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error restarting stack" + } + } + } + } + } + } + } }, body: t.Object({ stackId: t.Number(), @@ -195,6 +331,40 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Updates container images for a stack using Docker's pull mechanism (requires stack ID)", + responses: { + "200": { + description: "Successfully pulled images", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Images for stack 1 pulled successfully" + } + } + } + } + } + }, + "400": { + description: "Error pulling images", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error pulling images" + } + } + } + } + } + } + } }, body: t.Object({ stackId: t.Number(), @@ -236,6 +406,69 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Retrieves operational status for either a specific stack (by ID) or all managed stacks", + responses: { + "200": { + description: "Successfully retrieved stack status", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 status retrieved successfully" + }, + status: { + type: "object", + properties: { + name: { + type: "string", + example: "example-stack" + }, + status: { + type: "string", + example: "running" + }, + containers: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-stack_web_1" + }, + status: { + type: "string", + example: "running" + } + } + } + } + } + } + } + } + } + } + }, + "400": { + description: "Error getting stack status", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting stack status" + } + } + } + } + } + } + } }, query: t.Object({ stackId: t.Number(), @@ -260,6 +493,59 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Lists all registered stacks with their complete configuration details", + responses: { + "200": { + description: "Successfully retrieved stacks", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "number", + example: 1 + }, + name: { + type: "string", + example: "example-stack" + }, + version: { + type: "number", + example: 1 + }, + source: { + type: "string", + example: "github.com/example/repo" + }, + automatic_reboot_on_error: { + type: "boolean", + example: true + } + } + } + } + } + } + }, + "400": { + description: "Error getting stacks", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting stacks" + } + } + } + } + } + } + } }, }, ) @@ -283,6 +569,40 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) tags: ["Stacks"], description: "Permanently removes a stack configuration and cleans up associated resources", + responses: { + "200": { + description: "Successfully deleted stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 deleted successfully" + } + } + } + } + } + }, + "400": { + description: "Error deleting stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error deleting stack" + } + } + } + } + } + } + } }, body: t.Object({ stackId: t.Number(), diff --git a/src/routes/utils.ts b/src/routes/utils.ts index b578f92b..0f0ca26f 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -42,6 +42,81 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( tags: ["Utils"], description: "Retrieves DockStatAPI metadata including version, author information, dependencies, and licensing details", + responses: { + "200": { + description: "Successfully retrieved API information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0" + }, + authorEmail: { + type: "string", + example: "info@itsnik.de" + }, + authorName: { + type: "string", + example: "ItsNik" + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik" + }, + contributors: { + type: "array", + items: { + type: "string" + }, + example: [] + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0" + } + }, + description: { + type: "string", + example: "DockStatAPI is an API backend featuring plugins and more for DockStat" + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38" + } + }, + license: { + type: "string", + example: "CC BY-NC 4.0" + } + } + } + } + } + }, + "400": { + description: "Error retrieving API information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting DockStatAPI information" + } + } + } + } + } + } + } }, }, ); diff --git a/src/typings b/src/typings new file mode 160000 index 00000000..9cae829b --- /dev/null +++ b/src/typings @@ -0,0 +1 @@ +Subproject commit 9cae829bead60cd13351b757340f3225649cb11d diff --git a/src/typings/database.ts b/src/typings/database.ts deleted file mode 100644 index 880a801c..00000000 --- a/src/typings/database.ts +++ /dev/null @@ -1,27 +0,0 @@ -interface config { - keep_data_for: number; - fetching_interval: number; - api_key: string; -} - -interface stacks_config { - id: number; - name: string; - version: number; - custom: boolean; - source: string; - container_count: number; - stack_prefix: string; - automatic_reboot_on_error: boolean; - image_updates: boolean; -} - -interface log_message { - level: string; - timestamp: string; - message: string; - file: string; - line: number; -} - -export type { config, stacks_config, log_message }; diff --git a/src/typings/docker-compose.ts b/src/typings/docker-compose.ts deleted file mode 100644 index 8e6a5f93..00000000 --- a/src/typings/docker-compose.ts +++ /dev/null @@ -1,522 +0,0 @@ -export interface Stack { - compose_spec: ComposeSpec; - name: string; - version: number; - source: string; - id?: number; -} - -export interface ComposeSpec { - version?: string; - name?: string; - include?: Include[]; - services?: { [key: string]: Service }; - networks?: { [key: string]: Network }; - volumes?: { [key: string]: Volume }; - secrets?: { [key: string]: Secret }; - configs?: { [key: string]: Config }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -type Include = - | string - | { - path: string | string[]; - env_file?: string | string[]; - project_directory?: string; - }; - -interface Service { - develop?: Development | null; - deploy?: Deployment | null; - annotations?: ListOrDict; - attach?: boolean | string; - build?: - | string - | { - context?: string; - dockerfile?: string; - dockerfile_inline?: string; - entitlements?: string[]; - args?: ListOrDict; - ssh?: ListOrDict; - labels?: ListOrDict; - cache_from?: string[]; - cache_to?: string[]; - no_cache?: boolean | string; - additional_contexts?: ListOrDict; - network?: string; - pull?: boolean | string; - target?: string; - shm_size?: number | string; - extra_hosts?: ExtraHosts; - isolation?: string; - privileged?: boolean | string; - secrets?: ServiceConfigOrSecret[]; - tags?: string[]; - ulimits?: Ulimits; - platforms?: string[]; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - blkio_config?: { - device_read_bps?: BlkioLimit[]; - device_read_iops?: BlkioLimit[]; - device_write_bps?: BlkioLimit[]; - device_write_iops?: BlkioLimit[]; - weight?: number | string; - weight_device?: BlkioWeight[]; - }; - cap_add?: string[]; - cap_drop?: string[]; - cgroup?: "host" | "private"; - cgroup_parent?: string; - command?: Command; - configs?: ServiceConfigOrSecret[]; - container_name?: string; - cpu_count?: string | number; - cpu_percent?: string | number; - cpu_shares?: number | string; - cpu_quota?: number | string; - cpu_period?: number | string; - cpu_rt_period?: number | string; - cpu_rt_runtime?: number | string; - cpus?: number | string; - cpuset?: string; - credential_spec?: { - config?: string; - file?: string; - registry?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - depends_on?: - | string[] - | { - [service: string]: { - condition: - | "service_started" - | "service_healthy" - | "service_completed_successfully"; - restart?: boolean | string; - required?: boolean; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - }; - device_cgroup_rules?: string[]; - devices?: ( - | string - | { - source: string; - target?: string; - permissions?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - } - )[]; - dns?: StringOrList; - dns_opt?: string[]; - dns_search?: StringOrList; - domainname?: string; - entrypoint?: Command; - env_file?: EnvFile; - label_file?: string | string[]; - environment?: ListOrDict; - expose?: (string | number)[]; - extends?: string | { service: string; file?: string }; - external_links?: string[]; - extra_hosts?: ExtraHosts; - gpus?: - | "all" - | Array<{ - capabilities?: string[]; - count?: string | number; - device_ids?: string[]; - driver?: string; - options?: ListOrDict; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }>; - group_add?: (string | number)[]; - healthcheck?: Healthcheck; - hostname?: string; - image?: string; - init?: boolean | string; - ipc?: string; - isolation?: string; - labels?: ListOrDict; - links?: string[]; - logging?: { - driver?: string; - options?: { [key: string]: string | number | null }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - mac_address?: string; - mem_limit?: number | string; - mem_reservation?: string | number; - mem_swappiness?: number | string; - memswap_limit?: number | string; - network_mode?: string; - networks?: - | string[] - | { - [network: string]: { - aliases?: string[]; - ipv4_address?: string; - ipv6_address?: string; - link_local_ips?: string[]; - mac_address?: string; - driver_opts?: { [key: string]: string | number }; - priority?: number; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - } | null; - }; - oom_kill_disable?: boolean | string; - oom_score_adj?: string | number; - pid?: string | null; - pids_limit?: number | string; - platform?: string; - ports?: ( - | number - | string - | { - name?: string; - mode?: string; - host_ip?: string; - target?: number | string; - published?: string | number; - protocol?: string; - app_protocol?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - } - )[]; - post_start?: ServiceHook[]; - pre_stop?: ServiceHook[]; - privileged?: boolean | string; - profiles?: string[]; - pull_policy?: "always" | "never" | "if_not_present" | "build" | "missing"; - read_only?: boolean | string; - restart?: string; - runtime?: string; - scale?: number | string; - security_opt?: string[]; - shm_size?: number | string; - secrets?: ServiceConfigOrSecret[]; - sysctls?: ListOrDict; - stdin_open?: boolean | string; - stop_grace_period?: string; - stop_signal?: string; - storage_opt?: object; - tmpfs?: StringOrList; - tty?: boolean | string; - ulimits?: Ulimits; - user?: string; - uts?: string; - userns_mode?: string; - volumes?: ( - | string - | { - type: string; - source?: string; - target?: string; - read_only?: boolean | string; - consistency?: string; - bind?: { - propagation?: string; - create_host_path?: boolean | string; - recursive?: "enabled" | "disabled" | "writable" | "readonly"; - selinux?: "z" | "Z"; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - volume?: { - nocopy?: boolean | string; - subpath?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - tmpfs?: { - size?: number | string; - mode?: number | string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - } - )[]; - volumes_from?: string[]; - working_dir?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -interface Healthcheck { - disable?: boolean | string; - interval?: string; - retries?: number | string; - test?: string | string[]; - timeout?: string; - start_period?: string; - start_interval?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -interface Development { - watch?: Array<{ - path: string; - action: "rebuild" | "sync" | "restart" | "sync+restart" | "sync+exec"; - ignore?: string[]; - target?: string; - exec?: ServiceHook; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }>; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -interface Deployment { - mode?: string; - endpoint_mode?: string; - replicas?: number | string; - labels?: ListOrDict; - rollback_config?: { - parallelism?: number | string; - delay?: string; - failure_action?: string; - monitor?: string; - max_failure_ratio?: number | string; - order?: "start-first" | "stop-first"; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - update_config?: { - parallelism?: number | string; - delay?: string; - failure_action?: string; - monitor?: string; - max_failure_ratio?: number | string; - order?: "start-first" | "stop-first"; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - resources?: { - limits?: { - cpus?: number | string; - memory?: string; - pids?: number | string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - reservations?: { - cpus?: number | string; - memory?: string; - generic_resources?: Array<{ - discrete_resource_spec?: { - kind?: string; - value?: number | string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }>; - devices?: Array<{ - capabilities?: string[]; - count?: string | number; - device_ids?: string[]; - driver?: string; - options?: ListOrDict; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }>; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - restart_policy?: { - condition?: string; - delay?: string; - max_attempts?: number | string; - window?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - placement?: { - constraints?: string[]; - preferences?: Array<{ - spread?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }>; - max_replicas_per_node?: number | string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -type Command = string | string[] | null; -type EnvFile = - | string - | Array< - string | { path: string; format?: string; required?: boolean | string } - >; -type StringOrList = string | string[]; -type ListOrDict = - | { [key: string]: string | number | boolean | null } - | string[]; -type ExtraHosts = { [host: string]: string | string[] } | string[]; -interface BlkioLimit { - path: string; - rate: number | string; -} -interface BlkioWeight { - path: string; - weight: number | string; -} -type ServiceConfigOrSecret = - | string - | { - source: string; - target?: string; - uid?: string; - gid?: string; - mode?: number | string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; -type Ulimits = { - [key: string]: - | number - | string - | { hard: number | string; soft: number | string }; -}; - -interface ServiceHook { - command?: Command; - user?: string; - privileged?: boolean | string; - working_dir?: string; - environment?: ListOrDict; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -interface Network { - name?: string; - driver?: string; - driver_opts?: { [key: string]: string | number }; - ipam?: { - driver?: string; - config?: Array<{ - subnet?: string; - ip_range?: string; - gateway?: string; - aux_addresses?: { [key: string]: string }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }>; - options?: { [key: string]: string }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; - }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; - internal?: boolean | string; - enable_ipv4?: boolean | string; - enable_ipv6?: boolean | string; - attachable?: boolean | string; - labels?: ListOrDict; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -interface Volume { - name?: string; - driver?: string; - driver_opts?: { [key: string]: string | number }; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - external?: boolean | string | { name?: string; [key: `x-${string}`]: any }; - labels?: ListOrDict; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -interface Secret { - name?: string; - environment?: string; - file?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - external?: boolean | string | { name?: string; [key: string]: any }; - labels?: ListOrDict; - driver?: string; - driver_opts?: { [key: string]: string | number }; - template_driver?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} - -interface Config { - name?: string; - content?: string; - environment?: string; - file?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - external?: boolean | string | { name?: string; [key: string]: any }; - labels?: ListOrDict; - template_driver?: string; - - //biome-ignore lint/suspicious/noExplicitAny: Compose Spec - [key: `x-${string}`]: any; -} diff --git a/src/typings/docker.ts b/src/typings/docker.ts deleted file mode 100644 index 7e3b01f6..00000000 --- a/src/typings/docker.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { ContainerStats } from "dockerode"; -import type Docker from "dockerode"; - -interface DockerHost { - name: string; - hostAddress: string; - secure: boolean; - id: number; -} - -interface ContainerInfo { - id: string; - hostId: string; - name: string; - image: string; - status: string; - state: string; - cpuUsage: number; - memoryUsage: number; - stats?: ContainerStats; - info?: Docker.ContainerInfo; -} - -interface HostStats { - hostName: string; - hostId: number; - dockerVersion: string; - apiVersion: string; - os: string; - architecture: string; - totalMemory: number; - totalCPU: number; - labels: string[]; - containers: number; - containersRunning: number; - containersStopped: number; - containersPaused: number; - images: number; -} - -export type { HostStats, ContainerInfo, DockerHost }; diff --git a/src/typings/dockerode.ts b/src/typings/dockerode.ts deleted file mode 100644 index e1268ad6..00000000 --- a/src/typings/dockerode.ts +++ /dev/null @@ -1,162 +0,0 @@ -interface DockerInfo { - ID: string; - Containers: number; - ContainersRunning: number; - ContainersPaused: number; - ContainersStopped: number; - Images: number; - Driver: string; - DriverStatus: [string, string][]; - DockerRootDir: string; - SystemStatus: [string, string][]; - Plugins: { - Volume: string[]; - Network: string[]; - Authorization: string[]; - Log: string[]; - }; - MemoryLimit: boolean; - SwapLimit: boolean; - KernelMemory: boolean; - CpuCfsPeriod: boolean; - CpuCfsQuota: boolean; - CPUShares: boolean; - CPUSet: boolean; - OomKillDisable: boolean; - IPv4Forwarding: boolean; - BridgeNfIptables: boolean; - BridgeNfIp6tables: boolean; - Debug: boolean; - NFd: number; - NGoroutines: number; - SystemTime: string; - LoggingDriver: string; - CgroupDriver: string; - NEventsListener: number; - KernelVersion: string; - OperatingSystem: string; - OSType: string; - Architecture: string; - NCPU: number; - MemTotal: number; - IndexServerAddress: string; - RegistryConfig: { - AllowNondistributableArtifactsCIDRs: string[]; - AllowNondistributableArtifactsHostnames: string[]; - InsecureRegistryCIDRs: string[]; - IndexConfigs: Record< - string, - { - Name: string; - Mirrors: string[]; - Secure: boolean; - Official: boolean; - } - >; - Mirrors: string[]; - }; - GenericResources: Array< - | { DiscreteResourceSpec: { Kind: string; Value: number } } - | { NamedResourceSpec: { Kind: string; Value: string } } - >; - HttpProxy: string; - HttpsProxy: string; - NoProxy: string; - Name: string; - Labels: string[]; - ExperimentalBuild: boolean; - ServerVersion: string; - ClusterStore: string; - ClusterAdvertise: string; - Runtimes: Record< - string, - { - path: string; - runtimeArgs?: string[]; - } - >; - DefaultRuntime: string; - Swarm: { - NodeID: string; - NodeAddr: string; - LocalNodeState: string; - ControlAvailable: boolean; - Error: string; - RemoteManagers: Array<{ - NodeID: string; - Addr: string; - }>; - Nodes: number; - Managers: number; - Cluster: { - ID: string; - Version: { - Index: number; - }; - CreatedAt: string; - UpdatedAt: string; - Spec: { - Name: string; - Labels: Record; - Orchestration: { - TaskHistoryRetentionLimit: number; - }; - Raft: { - SnapshotInterval: number; - KeepOldSnapshots: number; - LogEntriesForSlowFollowers: number; - ElectionTick: number; - HeartbeatTick: number; - }; - Dispatcher: { - HeartbeatPeriod: number; - }; - CAConfig: { - NodeCertExpiry: number; - ExternalCAs: Array<{ - Protocol: string; - URL: string; - Options: Record; - CACert: string; - }>; - SigningCACert: string; - SigningCAKey: string; - ForceRotate: number; - }; - EncryptionConfig: { - AutoLockManagers: boolean; - }; - TaskDefaults: { - LogDriver: { - Name: string; - Options: Record; - }; - }; - }; - TLSInfo: { - TrustRoot: string; - CertIssuerSubject: string; - CertIssuerPublicKey: string; - }; - RootRotationInProgress: boolean; - }; - }; - LiveRestoreEnabled: boolean; - Isolation: string; - InitBinary: string; - ContainerdCommit: { - ID: string; - Expected: string; - }; - RuncCommit: { - ID: string; - Expected: string; - }; - InitCommit: { - ID: string; - Expected: string; - }; - SecurityOptions: string[]; -} - -export type { DockerInfo }; diff --git a/src/typings/elysiajs.ts b/src/typings/elysiajs.ts deleted file mode 100644 index a68bb8c5..00000000 --- a/src/typings/elysiajs.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { StatusMap } from "elysia"; -import type { ElysiaCookie } from "elysia/dist/cookies"; -import type { HTTPHeaders } from "elysia/dist/types"; - -interface set { - headers: HTTPHeaders; - status?: number | keyof StatusMap; - redirect?: string; - cookie?: Record; -} - -export type { set }; diff --git a/src/typings/misc.ts b/src/typings/misc.ts deleted file mode 100644 index c8bdc463..00000000 --- a/src/typings/misc.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type BackupInfo = { - filename: string; - date: Date; - backupNum: number; -}; diff --git a/src/typings/plugin.ts b/src/typings/plugin.ts deleted file mode 100644 index c5c3cc0f..00000000 --- a/src/typings/plugin.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ContainerInfo } from "~/typings/docker"; - -interface Plugin { - name: string; - - // Container lifecycle hooks - onContainerStart?: (containerInfo: ContainerInfo) => void; - onContainerStop?: (containerInfo: ContainerInfo) => void; - onContainerExit?: (containerInfo: ContainerInfo) => void; - onContainerCreate?: (containerInfo: ContainerInfo) => void; - onContainerKill?: (ContainerInfo: ContainerInfo) => void; - handleContainerDie?: (ContainerInfo: ContainerInfo) => void; - onContainerDestroy?: (containerInfo: ContainerInfo) => void; - onContainerPause?: (containerInfo: ContainerInfo) => void; - onContainerUnpause?: (containerInfo: ContainerInfo) => void; - onContainerRestart?: (containerInfo: ContainerInfo) => void; - onContainerUpdate?: (containerInfo: ContainerInfo) => void; - onContainerRename?: (containerInfo: ContainerInfo) => void; - onContainerHealthStatus?: (containerInfo: ContainerInfo) => void; - - // Host lifecycle hooks - onHostUnreachable?: (host: string, err: string) => void; - onHostReachableAgain?: (host: string) => void; -} - -export type { Plugin }; diff --git a/src/typings/websocket.ts b/src/typings/websocket.ts deleted file mode 100644 index e7ed96da..00000000 --- a/src/typings/websocket.ts +++ /dev/null @@ -1,15 +0,0 @@ -interface stackSocketMessage { - message?: string; - type?: "stack-progress" | "stack-error" | "stack-status" | "stack-removed"; - data?: stackSocketData; -} - -interface stackSocketData { - stack_id: number; - message: string; - action?: string; - status?: string; - timestamp?: string; -} - -export type { stackSocketMessage }; From 59e9ee1ce7b8e43ad4fd9fc05cfb82ccd3544a4b Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 30 Apr 2025 17:04:32 +0000 Subject: [PATCH 286/369] Update dependency graphs --- dependency-graph.mmd | 379 +++++++-------- dependency-graph.svg | 1101 +++++++++++++++++++++--------------------- 2 files changed, 740 insertions(+), 740 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index bb9e74a9..dcab5575 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -8,224 +8,225 @@ flowchart LR subgraph 0["src"] 1["index.ts"] -subgraph 2["routes"] -3["live-stacks.ts"] -S["live-logs.ts"] -1G["api-config.ts"] -1I["docker-manager.ts"] -1J["docker-stats.ts"] -1K["docker-websocket.ts"] -1M["logs.ts"] -1N["stacks.ts"] -1Q["utils.ts"] +subgraph 5["routes"] +6["live-stacks.ts"] +U["live-logs.ts"] +1H["api-config.ts"] +1J["docker-manager.ts"] +1K["docker-stats.ts"] +1L["docker-websocket.ts"] +1N["logs.ts"] +1O["stacks.ts"] +1R["utils.ts"] end -subgraph 4["core"] -subgraph 5["utils"] -6["logger.ts"] -Q["helpers.ts"] -14["calculations.ts"] -18["change-me-checker.ts"] -1A["package-json.ts"] -1C["swagger-readme.ts"] -1H["response-handler.ts"] +subgraph 8["core"] +subgraph 9["utils"] +A["logger.ts"] +T["helpers.ts"] +15["calculations.ts"] +19["change-me-checker.ts"] +1B["package-json.ts"] +1D["swagger-readme.ts"] +1I["response-handler.ts"] end -subgraph 8["database"] -9["_dbState.ts"] -A["index.ts"] -B["backup.ts"] -D["database.ts"] -F["helper.ts"] -I["config.ts"] -J["containerStats.ts"] -K["dockerHosts.ts"] -M["hostStats.ts"] -N["logs.ts"] -P["stacks.ts"] +subgraph C["database"] +D["_dbState.ts"] +E["index.ts"] +F["backup.ts"] +I["database.ts"] +K["helper.ts"] +L["config.ts"] +M["containerStats.ts"] +N["dockerHosts.ts"] +P["hostStats.ts"] +Q["logs.ts"] +R["stacks.ts"] end -subgraph U["docker"] -V["monitor.ts"] -11["client.ts"] -12["scheduler.ts"] -13["store-container-stats.ts"] -15["store-host-stats.ts"] +subgraph V["docker"] +W["monitor.ts"] +12["client.ts"] +13["scheduler.ts"] +14["store-container-stats.ts"] +16["store-host-stats.ts"] end -subgraph X["plugins"] -Y["plugin-manager.ts"] -17["loader.ts"] +subgraph Y["plugins"] +Z["plugin-manager.ts"] +18["loader.ts"] end -subgraph 1O["stacks"] -1P["controller.ts"] +subgraph 1P["stacks"] +1Q["controller.ts"] end end -subgraph G["typings"] -H["misc.ts"] -L["docker.ts"] -O["database.ts"] -R["docker-compose.ts"] -T["websocket.ts"] -10["plugin.ts"] -16["dockerode.ts"] -1F["elysiajs.ts"] +subgraph 1E["middleware"] +1F["auth.ts"] end -subgraph 1D["middleware"] -1E["auth.ts"] end +subgraph 2["~"] +subgraph 3["typings"] +4["database"] +7["websocket"] +G["misc"] +O["docker"] +S["docker-compose"] +10["plugin"] +17["dockerode"] +1G["elysiajs"] end -7["path"] -subgraph C["fs"] -19["promises"] end -E["bun:sqlite"] -W["bun"] -Z["events"] -1B["package.json"] -1L["stream"] -1-->3 -1-->A -1-->V -1-->12 -1-->17 +B["path"] +subgraph H["fs"] +1A["promises"] +end +J["bun:sqlite"] +X["bun"] +11["events"] +1C["package.json"] +1M["stream"] 1-->6 -1-->1A -1-->1C -1-->1E -1-->1G -1-->1I +1-->E +1-->W +1-->13 +1-->18 +1-->A +1-->1B +1-->1D +1-->1F +1-->1H 1-->1J 1-->1K -1-->S -1-->1M +1-->1L +1-->U 1-->1N -1-->1Q -1-->O -3-->6 -3-->T -6-->9 +1-->1O +1-->1R +1-->4 6-->A -6-->S -6-->O 6-->7 -A-->B -A-->I -A-->J A-->D -A-->K -A-->M -A-->N -A-->P -B-->9 -B-->D -B-->F -B-->6 -B-->H -B-->C -D-->E -D-->C -F-->9 -F-->6 -I-->D -I-->F -J-->D -J-->F +A-->E +A-->U +A-->4 +A-->B +E-->F +E-->L +E-->M +E-->I +E-->N +E-->P +E-->Q +E-->R +F-->D +F-->I +F-->K +F-->A +F-->G +F-->H +I-->J +I-->H K-->D -K-->F -K-->L -M-->D -M-->F -M-->L -N-->D -N-->F +K-->A +L-->I +L-->K +M-->I +M-->K +N-->I +N-->K N-->O -P-->Q -P-->D -P-->F +P-->I +P-->K P-->O -P-->R -Q-->6 -S-->6 -S-->O -V-->Y -V-->A -V-->11 -V-->6 -V-->L -V-->W -Y-->6 -Y-->L -Y-->10 -Y-->Z -10-->L -11-->6 -11-->L +Q-->I +Q-->K +Q-->4 +R-->T +R-->I +R-->K +R-->4 +R-->S +T-->A +U-->A +U-->4 +W-->Z +W-->E +W-->12 +W-->A +W-->O +W-->X +Z-->A +Z-->O +Z-->10 +Z-->11 12-->A -12-->13 -12-->15 -12-->6 12-->O -13-->6 -13-->A -13-->11 +13-->E 13-->14 -15-->A -15-->11 -15-->Q -15-->6 -15-->L -15-->16 -17-->18 -17-->6 -17-->Y -17-->C -17-->7 -18-->6 +13-->16 +13-->A +13-->4 +14-->A +14-->E +14-->12 +14-->15 +16-->E +16-->12 +16-->T +16-->A +16-->O +16-->17 18-->19 -1A-->1B -1E-->A -1E-->6 -1E-->O -1E-->1F -1G-->A -1G-->B -1G-->Y -1G-->6 -1G-->1A -1G-->1H -1G-->1E -1G-->O -1G-->C -1H-->6 +18-->A +18-->Z +18-->H +18-->B +19-->A +19-->1A +1B-->1C +1F-->E +1F-->A +1F-->4 +1F-->1G +1H-->E +1H-->F +1H-->Z +1H-->A +1H-->1B +1H-->1I 1H-->1F +1H-->4 +1H-->H 1I-->A -1I-->6 -1I-->1H -1I-->L +1I-->1G +1J-->E 1J-->A -1J-->11 -1J-->14 -1J-->Q -1J-->6 -1J-->1H -1J-->L -1J-->16 +1J-->1I +1J-->O +1K-->E +1K-->12 +1K-->15 +1K-->T 1K-->A -1K-->11 -1K-->14 -1K-->6 -1K-->1H -1K-->1L -1M-->A -1M-->6 +1K-->1I +1K-->O +1K-->17 +1L-->E +1L-->12 +1L-->15 +1L-->A +1L-->1I +1L-->1M +1N-->E 1N-->A -1N-->1P -1N-->6 -1N-->1H -1P-->Q -1P-->A -1P-->6 -1P-->3 -1P-->O -1P-->R -1P-->19 +1O-->E +1O-->1Q +1O-->A +1O-->1I +1Q-->T +1Q-->E +1Q-->A +1Q-->6 +1Q-->4 +1Q-->S 1Q-->1A -1Q-->1H +1R-->1B +1R-->1I diff --git a/dependency-graph.svg b/dependency-graph.svg index 3aa239a0..7a12bb42 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,72 +4,77 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes -cluster_src/typings - -typings +cluster_~ + +~ + + +cluster_~/typings + +typings bun - -bun + +bun @@ -77,8 +82,8 @@ bun:sqlite - -bun:sqlite + +bun:sqlite @@ -86,8 +91,8 @@ events - -events + +events @@ -95,8 +100,8 @@ fs - -fs + +fs @@ -104,8 +109,8 @@ fs/promises - -promises + +promises @@ -113,8 +118,8 @@ package.json - -package.json + +package.json @@ -122,8 +127,8 @@ path - -path + +path @@ -131,8 +136,8 @@ src/core/database/_dbState.ts - -_dbState.ts + +_dbState.ts @@ -140,902 +145,896 @@ src/core/database/backup.ts - -backup.ts + +backup.ts src/core/database/backup.ts->fs - - + + src/core/database/backup.ts->src/core/database/_dbState.ts - - + + src/core/database/database.ts - -database.ts + +database.ts src/core/database/backup.ts->src/core/database/database.ts - - + + src/core/database/helper.ts - -helper.ts + +helper.ts src/core/database/backup.ts->src/core/database/helper.ts - - - - + + + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + - + -src/typings/misc.ts - - -misc.ts +~/typings/misc + + +misc - + -src/core/database/backup.ts->src/typings/misc.ts - - +src/core/database/backup.ts->~/typings/misc + + src/core/database/database.ts->bun:sqlite - - + + src/core/database/database.ts->fs - - + + src/core/database/helper.ts->src/core/database/_dbState.ts - - + + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/utils/logger.ts->path - - + + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + src/core/database/index.ts - -index.ts + +index.ts src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + - + -src/typings/database.ts - - -database.ts +~/typings/database + + +database - + -src/core/utils/logger.ts->src/typings/database.ts - - +src/core/utils/logger.ts->~/typings/database + + src/routes/live-logs.ts - -live-logs.ts + +live-logs.ts src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/core/database/config.ts - -config.ts + +config.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + - + -src/typings/docker.ts - - -docker.ts +~/typings/docker + + +docker - + -src/core/database/dockerHosts.ts->src/typings/docker.ts - - +src/core/database/dockerHosts.ts->~/typings/docker + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + - + -src/core/database/hostStats.ts->src/typings/docker.ts - - +src/core/database/hostStats.ts->~/typings/docker + + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + - + -src/core/database/logs.ts->src/typings/database.ts - - +src/core/database/logs.ts->~/typings/database + + src/core/database/stacks.ts->src/core/database/database.ts - - + + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + - + -src/core/database/stacks.ts->src/typings/database.ts - - +src/core/database/stacks.ts->~/typings/database + + src/core/utils/helpers.ts - -helpers.ts + +helpers.ts src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - + + + + - + -src/typings/docker-compose.ts - - -docker-compose.ts +~/typings/docker-compose + + +docker-compose - + -src/core/database/stacks.ts->src/typings/docker-compose.ts - - +src/core/database/stacks.ts->~/typings/docker-compose + + src/core/utils/helpers.ts->src/core/utils/logger.ts - - - - + + + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + - + -src/core/docker/client.ts->src/typings/docker.ts - - +src/core/docker/client.ts->~/typings/docker + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->bun - - + + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + - + -src/core/docker/monitor.ts->src/typings/docker.ts - - +src/core/docker/monitor.ts->~/typings/docker + + src/core/docker/monitor.ts->src/core/database/index.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + src/core/plugins/plugin-manager.ts->events - - + + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + -src/core/plugins/plugin-manager.ts->src/typings/docker.ts - - +src/core/plugins/plugin-manager.ts->~/typings/docker + + - + -src/typings/plugin.ts - - -plugin.ts +~/typings/plugin + + +plugin - + -src/core/plugins/plugin-manager.ts->src/typings/plugin.ts - - +src/core/plugins/plugin-manager.ts->~/typings/plugin + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + - + -src/core/docker/scheduler.ts->src/typings/database.ts - - +src/core/docker/scheduler.ts->~/typings/database + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + -src/core/docker/store-host-stats.ts->src/typings/docker.ts - - +src/core/docker/store-host-stats.ts->~/typings/docker + + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + -src/typings/dockerode.ts - - -dockerode.ts +~/typings/dockerode + + +dockerode - + -src/core/docker/store-host-stats.ts->src/typings/dockerode.ts - - +src/core/docker/store-host-stats.ts->~/typings/dockerode + + src/core/plugins/loader.ts - -loader.ts + +loader.ts src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + src/core/utils/change-me-checker.ts->fs/promises - - + + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - - - - -src/typings/plugin.ts->src/typings/docker.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts src/core/stacks/controller.ts->fs/promises - - + + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts->src/core/database/index.ts - - + + - + -src/core/stacks/controller.ts->src/typings/database.ts - - +src/core/stacks/controller.ts->~/typings/database + + src/core/stacks/controller.ts->src/core/utils/helpers.ts - - + + - + -src/core/stacks/controller.ts->src/typings/docker-compose.ts - - +src/core/stacks/controller.ts->~/typings/docker-compose + + src/routes/live-stacks.ts - -live-stacks.ts + +live-stacks.ts src/core/stacks/controller.ts->src/routes/live-stacks.ts - - + + src/routes/live-stacks.ts->src/core/utils/logger.ts - - + + - + -src/typings/websocket.ts - - -websocket.ts +~/typings/websocket + + +websocket - + -src/routes/live-stacks.ts->src/typings/websocket.ts - - +src/routes/live-stacks.ts->~/typings/websocket + + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + -src/routes/live-logs.ts->src/typings/database.ts - - +src/routes/live-logs.ts->~/typings/database + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts src/core/utils/package-json.ts->package.json - - + + src/core/utils/response-handler.ts - -response-handler.ts + +response-handler.ts src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + -src/typings/elysiajs.ts - - -elysiajs.ts +~/typings/elysiajs + + +elysiajs - + -src/core/utils/response-handler.ts->src/typings/elysiajs.ts - - +src/core/utils/response-handler.ts->~/typings/elysiajs + + src/core/utils/swagger-readme.ts - -swagger-readme.ts + +swagger-readme.ts @@ -1043,433 +1042,433 @@ src/index.ts - -index.ts + +index.ts src/index.ts->src/core/utils/logger.ts - - + + src/index.ts->src/core/database/index.ts - - + + - + -src/index.ts->src/typings/database.ts - - +src/index.ts->~/typings/database + + src/index.ts->src/core/docker/monitor.ts - - + + src/index.ts->src/core/docker/scheduler.ts - - + + src/index.ts->src/core/plugins/loader.ts - - + + src/index.ts->src/routes/live-stacks.ts - - + + src/index.ts->src/routes/live-logs.ts - - + + src/index.ts->src/core/utils/package-json.ts - - + + src/index.ts->src/core/utils/swagger-readme.ts - - + + src/middleware/auth.ts - -auth.ts + +auth.ts src/index.ts->src/middleware/auth.ts - - + + src/routes/api-config.ts - -api-config.ts + +api-config.ts src/index.ts->src/routes/api-config.ts - - + + src/routes/docker-manager.ts - -docker-manager.ts + +docker-manager.ts src/index.ts->src/routes/docker-manager.ts - - + + src/routes/docker-stats.ts - -docker-stats.ts + +docker-stats.ts src/index.ts->src/routes/docker-stats.ts - - + + src/routes/docker-websocket.ts - -docker-websocket.ts + +docker-websocket.ts src/index.ts->src/routes/docker-websocket.ts - - + + src/routes/logs.ts - -logs.ts + +logs.ts src/index.ts->src/routes/logs.ts - - + + src/routes/stacks.ts - -stacks.ts + +stacks.ts src/index.ts->src/routes/stacks.ts - - + + src/routes/utils.ts - -utils.ts + +utils.ts src/index.ts->src/routes/utils.ts - - + + src/middleware/auth.ts->src/core/utils/logger.ts - - + + src/middleware/auth.ts->src/core/database/index.ts - - + + - + -src/middleware/auth.ts->src/typings/database.ts - - +src/middleware/auth.ts->~/typings/database + + - + -src/middleware/auth.ts->src/typings/elysiajs.ts - - +src/middleware/auth.ts->~/typings/elysiajs + + src/routes/api-config.ts->fs - - + + src/routes/api-config.ts->src/core/database/backup.ts - - + + src/routes/api-config.ts->src/core/utils/logger.ts - - + + src/routes/api-config.ts->src/core/database/index.ts - - + + - + -src/routes/api-config.ts->src/typings/database.ts - - +src/routes/api-config.ts->~/typings/database + + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + src/routes/api-config.ts->src/middleware/auth.ts - - + + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + -src/routes/docker-manager.ts->src/typings/docker.ts - - +src/routes/docker-manager.ts->~/typings/docker + + src/routes/docker-manager.ts->src/core/database/index.ts - - + + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + -src/routes/docker-stats.ts->src/typings/docker.ts - - +src/routes/docker-stats.ts->~/typings/docker + + src/routes/docker-stats.ts->src/core/database/index.ts - - + + src/routes/docker-stats.ts->src/core/utils/helpers.ts - - + + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + -src/routes/docker-stats.ts->src/typings/dockerode.ts - - +src/routes/docker-stats.ts->~/typings/dockerode + + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + stream - -stream + +stream src/routes/docker-websocket.ts->stream - - + + src/routes/logs.ts->src/core/utils/logger.ts - - + + src/routes/logs.ts->src/core/database/index.ts - - + + src/routes/stacks.ts->src/core/utils/logger.ts - - + + src/routes/stacks.ts->src/core/database/index.ts - - + + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + src/routes/utils.ts->src/core/utils/package-json.ts - - + + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + From a24c66e4b141489b02e7654b63b2d22cd63a3b61 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 30 Apr 2025 19:07:25 +0200 Subject: [PATCH 287/369] Chroe: Update CodeQL --- package.json | 6 +- src/core/database/logs.ts | 14 +- src/core/utils/logger.ts | 13 +- src/index.ts | 8 +- src/routes/api-config.ts | 260 ++++++++++++++++++----------------- src/routes/docker-manager.ts | 120 ++++++++-------- src/routes/docker-stats.ts | 104 +++++++------- src/routes/live-logs.ts | 8 +- src/routes/logs.ts | 128 ++++++++--------- src/routes/stacks.ts | 252 ++++++++++++++++----------------- src/routes/utils.ts | 51 +++---- 11 files changed, 491 insertions(+), 473 deletions(-) diff --git a/package.json b/package.json index 6bf8d3bc..a3e18f4f 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,5 @@ "wrap-ansi": "^9.0.0" }, "module": "src/index.js", - "trustedDependencies": [ - "protobufjs" - ] -} \ No newline at end of file + "trustedDependencies": ["protobufjs"] +} diff --git a/src/core/database/logs.ts b/src/core/database/logs.ts index ad100c4d..f6b99802 100644 --- a/src/core/database/logs.ts +++ b/src/core/database/logs.ts @@ -16,7 +16,7 @@ const stmt = { deleteByLevel: db.prepare("DELETE FROM backend_log_entries WHERE level = ?"), }; -function convertToLogMessage(row: any): log_message { +function convertToLogMessage(row: log_message): log_message { return { level: row.level, timestamp: row.timestamp, @@ -55,16 +55,16 @@ export function addLogEntry(data: log_message) { } export function getAllLogs(): log_message[] { - return executeDbOperation( - "Get All Logs", - () => stmt.selectAll.all().map(convertToLogMessage), + return executeDbOperation("Get All Logs", () => + stmt.selectAll.all().map((row) => convertToLogMessage(row as log_message)), ); } export function getLogsByLevel(level: string): log_message[] { - return executeDbOperation( - "Get Logs By Level", - () => stmt.selectByLevel.all(level).map(convertToLogMessage), + return executeDbOperation("Get Logs By Level", () => + stmt.selectByLevel + .all(level) + .map((row) => convertToLogMessage(row as log_message)), ); } diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 30e60457..e8b8404e 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -69,7 +69,14 @@ const parseTimestamp = (timestamp: string): string => { const [day, month] = datePart.split("/"); const [hours, minutes, seconds] = timePart.split(":"); const year = new Date().getFullYear(); - const date = new Date(year, parseInt(month) - 1, parseInt(day), parseInt(hours), parseInt(minutes), parseInt(seconds)); + const date = new Date( + year, + parseInt(month) - 1, + parseInt(day), + parseInt(hours), + parseInt(minutes), + parseInt(seconds), + ); return date.toISOString(); }; @@ -77,7 +84,7 @@ const handleWebSocketLog = (log: log_message) => { try { logToClients({ ...log, - timestamp: parseTimestamp(log.timestamp) + timestamp: parseTimestamp(log.timestamp), }); } catch (error) { console.error( @@ -95,7 +102,7 @@ const handleDatabaseLog = (log: log_message): void => { try { dbFunctions.addLogEntry({ ...log, - timestamp: parseTimestamp(log.timestamp) + timestamp: parseTimestamp(log.timestamp), }); } catch (error) { console.error( diff --git a/src/index.ts b/src/index.ts index 062d8b88..badec71a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,7 +76,7 @@ export const DockStatAPI = new Elysia() { name: "Utils", description: "Various utilities which might be useful", - } + }, ], }, }), @@ -84,7 +84,11 @@ export const DockStatAPI = new Elysia() .onBeforeHandle(async (context) => { const { path, request, set } = context; - if (path === "/health" || path.startsWith("/swagger") || path.startsWith("/trpc")) { + if ( + path === "/health" || + path.startsWith("/swagger") || + path.startsWith("/trpc") + ) { logger.info(`Requested unguarded route: ${path}`); return; } diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 354ffb02..5ccca3e5 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -54,20 +54,20 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { fetching_interval: { type: "number", - example: 5 + example: 5, }, keep_data_for: { type: "number", - example: 7 + example: 7, }, api_key: { type: "string", - example: "hashed_api_key" - } - } - } - } - } + example: "hashed_api_key", + }, + }, + }, + }, + }, }, "400": { description: "Error retrieving configuration", @@ -78,14 +78,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Error getting the DockStatAPI config" - } - } - } - } - } - } - } + example: "Error getting the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -119,21 +119,21 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { name: { type: "string", - example: "example-plugin" + example: "example-plugin", }, version: { type: "string", - example: "1.0.0" + example: "1.0.0", }, status: { type: "string", - example: "active" - } - } - } - } - } - } + example: "active", + }, + }, + }, + }, + }, + }, }, "400": { description: "Error retrieving plugins", @@ -144,14 +144,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Error getting all registered plugins" - } - } - } - } - } - } - } + example: "Error getting all registered plugins", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -190,12 +190,12 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { message: { type: "string", - example: "Updated DockStatAPI config" - } - } - } - } - } + example: "Updated DockStatAPI config", + }, + }, + }, + }, + }, }, "400": { description: "Error updating configuration", @@ -206,14 +206,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Error updating the DockStatAPI config" - } - } - } - } - } - } - } + example: "Error updating the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ fetching_interval: t.Number(), @@ -261,53 +261,54 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { version: { type: "string", - example: "3.0.0" + example: "3.0.0", }, description: { type: "string", - example: "DockStatAPI is an API backend featuring plugins and more for DockStat" + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", }, license: { type: "string", - example: "CC BY-NC 4.0" + example: "CC BY-NC 4.0", }, authorName: { type: "string", - example: "ItsNik" + example: "ItsNik", }, authorEmail: { type: "string", - example: "info@itsnik.de" + example: "info@itsnik.de", }, authorWebsite: { type: "string", - example: "https://github.com/Its4Nik" + example: "https://github.com/Its4Nik", }, contributors: { type: "array", items: { - type: "string" + type: "string", }, - example: [] + example: [], }, dependencies: { type: "object", example: { "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0" - } + "@elysiajs/static": "^1.2.0", + }, }, devDependencies: { type: "object", example: { "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38" - } - } - } - } - } - } + "@types/dockerode": "^3.3.38", + }, + }, + }, + }, + }, + }, }, "400": { description: "Error retrieving package information", @@ -318,14 +319,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Error while reading package.json" - } - } - } - } - } - } - } + example: "Error while reading package.json", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -353,12 +354,12 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { message: { type: "string", - example: "backup_2024-03-20_12-00-00.db.bak" - } - } - } - } - } + example: "backup_2024-03-20_12-00-00.db.bak", + }, + }, + }, + }, + }, }, "400": { description: "Error creating backup", @@ -369,14 +370,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Error backing up" - } - } - } - } - } - } - } + example: "Error backing up", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -415,15 +416,15 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) schema: { type: "array", items: { - type: "string" + type: "string", }, example: [ "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak" - ] - } - } - } + "backup_2024-03-19_12-00-00.db.bak", + ], + }, + }, + }, }, "400": { description: "Error retrieving backup list", @@ -434,14 +435,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Reading Backup directory" - } - } - } - } - } - } - } + example: "Reading Backup directory", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -482,18 +483,19 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) schema: { type: "string", format: "binary", - example: "Binary backup file content" - } - } + example: "Binary backup file content", + }, + }, }, headers: { "Content-Disposition": { schema: { type: "string", - example: "attachment; filename=\"backup_2024-03-20_12-00-00.db.bak\"" - } - } - } + example: + 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', + }, + }, + }, }, "400": { description: "Error downloading backup", @@ -504,14 +506,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Backup download failed" - } - } - } - } - } - } - } + example: "Backup download failed", + }, + }, + }, + }, + }, + }, + }, }, query: t.Object({ filename: t.Optional(t.String()), @@ -565,12 +567,12 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { message: { type: "string", - example: "Database restored successfully" - } - } - } - } - } + example: "Database restored successfully", + }, + }, + }, + }, + }, }, "400": { description: "Error restoring database", @@ -581,14 +583,14 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) properties: { error: { type: "string", - example: "Database restoration error" - } - } - } - } - } - } - } + example: "Database restoration error", + }, + }, + }, + }, + }, + }, + }, }, }, ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 13f33125..68044cdf 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -37,12 +37,12 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { message: { type: "string", - example: "Added docker host (Localhost)" - } - } - } - } - } + example: "Added docker host (Localhost)", + }, + }, + }, + }, + }, }, "400": { description: "Error adding Docker host", @@ -53,14 +53,14 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { error: { type: "string", - example: "Error adding docker Host" - } - } - } - } - } - } - } + example: "Error adding docker Host", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ name: t.String(), @@ -100,12 +100,12 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { message: { type: "string", - example: "Updated docker host (1)" - } - } - } - } - } + example: "Updated docker host (1)", + }, + }, + }, + }, + }, }, "400": { description: "Error updating Docker host", @@ -116,14 +116,14 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { error: { type: "string", - example: "Failed to update host" - } - } - } - } - } - } - } + example: "Failed to update host", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ id: t.Number(), @@ -167,25 +167,25 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { id: { type: "number", - example: 1 + example: 1, }, name: { type: "string", - example: "Localhost" + example: "Localhost", }, hostAddress: { type: "string", - example: "localhost:2375" + example: "localhost:2375", }, secure: { type: "boolean", - example: false - } - } - } - } - } - } + example: false, + }, + }, + }, + }, + }, + }, }, "400": { description: "Error retrieving Docker hosts", @@ -196,14 +196,14 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { error: { type: "string", - example: "Failed to retrieve hosts" - } - } - } - } - } - } - } + example: "Failed to retrieve hosts", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -238,12 +238,12 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { message: { type: "string", - example: "Deleted docker host (1)" - } - } - } - } - } + example: "Deleted docker host (1)", + }, + }, + }, + }, + }, }, "400": { description: "Error deleting Docker host", @@ -254,14 +254,14 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) properties: { error: { type: "string", - example: "Failed to delete host" - } - } - } - } - } - } - } + example: "Failed to delete host", + }, + }, + }, + }, + }, + }, + }, }, params: t.Object({ id: t.Number(), diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index db108161..3781f26b 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -123,43 +123,43 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) properties: { id: { type: "string", - example: "abc123def456" + example: "abc123def456", }, hostId: { type: "string", - example: "1" + example: "1", }, name: { type: "string", - example: "example-container" + example: "example-container", }, image: { type: "string", - example: "nginx:latest" + example: "nginx:latest", }, status: { type: "string", - example: "running" + example: "running", }, state: { type: "string", - example: "running" + example: "running", }, cpuUsage: { type: "number", - example: 0.5 + example: 0.5, }, memoryUsage: { type: "number", - example: 1024 - } - } - } - } - } - } - } - } + example: 1024, + }, + }, + }, + }, + }, + }, + }, + }, }, "400": { description: "Error retrieving container statistics", @@ -170,14 +170,14 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) properties: { error: { type: "string", - example: "Failed to retrieve containers" - } - } - } - } - } - } - } + example: "Failed to retrieve containers", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -241,67 +241,67 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) properties: { hostId: { type: "number", - example: 1 + example: 1, }, hostName: { type: "string", - example: "Localhost" + example: "Localhost", }, dockerVersion: { type: "string", - example: "24.0.5" + example: "24.0.5", }, apiVersion: { type: "string", - example: "1.41" + example: "1.41", }, os: { type: "string", - example: "Linux" + example: "Linux", }, architecture: { type: "string", - example: "x86_64" + example: "x86_64", }, totalMemory: { type: "number", - example: 16777216 + example: 16777216, }, totalCPU: { type: "number", - example: 4 + example: 4, }, labels: { type: "array", items: { - type: "string" + type: "string", }, - example: ["environment=production"] + example: ["environment=production"], }, images: { type: "number", - example: 10 + example: 10, }, containers: { type: "number", - example: 5 + example: 5, }, containersPaused: { type: "number", - example: 0 + example: 0, }, containersRunning: { type: "number", - example: 4 + example: 4, }, containersStopped: { type: "number", - example: 1 - } - } - } - } - } + example: 1, + }, + }, + }, + }, + }, }, "400": { description: "Error retrieving host statistics", @@ -312,14 +312,14 @@ export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) properties: { error: { type: "string", - example: "Failed to retrieve host config" - } - } - } - } - } - } - } + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, }, }, ); diff --git a/src/routes/live-logs.ts b/src/routes/live-logs.ts index 50e9ea84..cf6e4b5b 100644 --- a/src/routes/live-logs.ts +++ b/src/routes/live-logs.ts @@ -11,7 +11,13 @@ const activeConnections = new Set>(); export const liveLogs = new Elysia({ prefix: "/logs" }).ws("/ws", { open(ws) { activeConnections.add(ws); - ws.send({ message: "Connection established", level: "info", timestamp: new Date().toISOString(), file: "live-logs.ts", line: 14 }); + ws.send({ + message: "Connection established", + level: "info", + timestamp: new Date().toISOString(), + file: "live-logs.ts", + line: 14, + }); logger.info(`New Logs WebSocket established (${ws.id})`); }, close(ws) { diff --git a/src/routes/logs.ts b/src/routes/logs.ts index 20b3be2f..a4feb152 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -35,25 +35,25 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { id: { type: "number", - example: 1 + example: 1, }, level: { type: "string", - example: "info" + example: "info", }, message: { type: "string", - example: "Application started" + example: "Application started", }, timestamp: { type: "string", - example: "2024-03-20T12:00:00Z" - } - } - } - } - } - } + example: "2024-03-20T12:00:00Z", + }, + }, + }, + }, + }, + }, }, "500": { description: "Error retrieving logs", @@ -64,14 +64,14 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { error: { type: "string", - example: "Failed to retrieve logs" - } - } - } - } - } - } - } + example: "Failed to retrieve logs", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -107,25 +107,25 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { id: { type: "number", - example: 1 + example: 1, }, level: { type: "string", - example: "info" + example: "info", }, message: { type: "string", - example: "Application started" + example: "Application started", }, timestamp: { type: "string", - example: "2024-03-20T12:00:00Z" - } - } - } - } - } - } + example: "2024-03-20T12:00:00Z", + }, + }, + }, + }, + }, + }, }, "500": { description: "Error retrieving logs", @@ -136,14 +136,14 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { error: { type: "string", - example: "Failed to retrieve logs" - } - } - } - } - } - } - } + example: "Failed to retrieve logs", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -176,12 +176,12 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { success: { type: "boolean", - example: true - } - } - } - } - } + example: true, + }, + }, + }, + }, + }, }, "500": { description: "Error clearing logs", @@ -192,14 +192,14 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { error: { type: "string", - example: "Could not delete all logs" - } - } - } - } - } - } - } + example: "Could not delete all logs", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -232,12 +232,12 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { success: { type: "boolean", - example: true - } - } - } - } - } + example: true, + }, + }, + }, + }, + }, }, "500": { description: "Error clearing logs", @@ -248,14 +248,14 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) properties: { error: { type: "string", - example: "Failed to retrieve logs" - } - } - } - } - } - } - } + example: "Failed to retrieve logs", + }, + }, + }, + }, + }, + }, + }, }, }, ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 52eaf860..d5b22421 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -78,12 +78,12 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { message: { type: "string", - example: "Stack example-stack deployed successfully" - } - } - } - } - } + example: "Stack example-stack deployed successfully", + }, + }, + }, + }, + }, }, "400": { description: "Error deploying stack", @@ -94,14 +94,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error deploying stack" - } - } - } - } - } - } - } + example: "Error deploying stack", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ compose_spec: t.Any(), @@ -149,12 +149,12 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { message: { type: "string", - example: "Stack 1 started successfully" - } - } - } - } - } + example: "Stack 1 started successfully", + }, + }, + }, + }, + }, }, "400": { description: "Error starting stack", @@ -165,14 +165,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error starting stack" - } - } - } - } - } - } - } + example: "Error starting stack", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ stackId: t.Number(), @@ -213,12 +213,12 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { message: { type: "string", - example: "Stack 1 stopped successfully" - } - } - } - } - } + example: "Stack 1 stopped successfully", + }, + }, + }, + }, + }, }, "400": { description: "Error stopping stack", @@ -229,14 +229,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error stopping stack" - } - } - } - } - } - } - } + example: "Error stopping stack", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ stackId: t.Number(), @@ -277,12 +277,12 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { message: { type: "string", - example: "Stack 1 restarted successfully" - } - } - } - } - } + example: "Stack 1 restarted successfully", + }, + }, + }, + }, + }, }, "400": { description: "Error restarting stack", @@ -293,14 +293,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error restarting stack" - } - } - } - } - } - } - } + example: "Error restarting stack", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ stackId: t.Number(), @@ -341,12 +341,12 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { message: { type: "string", - example: "Images for stack 1 pulled successfully" - } - } - } - } - } + example: "Images for stack 1 pulled successfully", + }, + }, + }, + }, + }, }, "400": { description: "Error pulling images", @@ -357,14 +357,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error pulling images" - } - } - } - } - } - } - } + example: "Error pulling images", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ stackId: t.Number(), @@ -416,18 +416,18 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { message: { type: "string", - example: "Stack 1 status retrieved successfully" + example: "Stack 1 status retrieved successfully", }, status: { type: "object", properties: { name: { type: "string", - example: "example-stack" + example: "example-stack", }, status: { type: "string", - example: "running" + example: "running", }, containers: { type: "array", @@ -436,21 +436,21 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { name: { type: "string", - example: "example-stack_web_1" + example: "example-stack_web_1", }, status: { type: "string", - example: "running" - } - } - } - } - } - } - } - } - } - } + example: "running", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, }, "400": { description: "Error getting stack status", @@ -461,14 +461,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error getting stack status" - } - } - } - } - } - } - } + example: "Error getting stack status", + }, + }, + }, + }, + }, + }, + }, }, query: t.Object({ stackId: t.Number(), @@ -505,29 +505,29 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { id: { type: "number", - example: 1 + example: 1, }, name: { type: "string", - example: "example-stack" + example: "example-stack", }, version: { type: "number", - example: 1 + example: 1, }, source: { type: "string", - example: "github.com/example/repo" + example: "github.com/example/repo", }, automatic_reboot_on_error: { type: "boolean", - example: true - } - } - } - } - } - } + example: true, + }, + }, + }, + }, + }, + }, }, "400": { description: "Error getting stacks", @@ -538,14 +538,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error getting stacks" - } - } - } - } - } - } - } + example: "Error getting stacks", + }, + }, + }, + }, + }, + }, + }, }, }, ) @@ -579,12 +579,12 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { message: { type: "string", - example: "Stack 1 deleted successfully" - } - } - } - } - } + example: "Stack 1 deleted successfully", + }, + }, + }, + }, + }, }, "400": { description: "Error deleting stack", @@ -595,14 +595,14 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) properties: { error: { type: "string", - example: "Error deleting stack" - } - } - } - } - } - } - } + example: "Error deleting stack", + }, + }, + }, + }, + }, + }, + }, }, body: t.Object({ stackId: t.Number(), diff --git a/src/routes/utils.ts b/src/routes/utils.ts index 0f0ca26f..591efd51 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -52,53 +52,54 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( properties: { version: { type: "string", - example: "3.0.0" + example: "3.0.0", }, authorEmail: { type: "string", - example: "info@itsnik.de" + example: "info@itsnik.de", }, authorName: { type: "string", - example: "ItsNik" + example: "ItsNik", }, authorWebsite: { type: "string", - example: "https://github.com/Its4Nik" + example: "https://github.com/Its4Nik", }, contributors: { type: "array", items: { - type: "string" + type: "string", }, - example: [] + example: [], }, dependencies: { type: "object", example: { "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0" - } + "@elysiajs/static": "^1.2.0", + }, }, description: { type: "string", - example: "DockStatAPI is an API backend featuring plugins and more for DockStat" + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", }, devDependencies: { type: "object", example: { "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38" - } + "@types/dockerode": "^3.3.38", + }, }, license: { type: "string", - example: "CC BY-NC 4.0" - } - } - } - } - } + example: "CC BY-NC 4.0", + }, + }, + }, + }, + }, }, "400": { description: "Error retrieving API information", @@ -109,14 +110,14 @@ export const utilRoutes = new Elysia({ prefix: "/utils" }).get( properties: { error: { type: "string", - example: "Error getting DockStatAPI information" - } - } - } - } - } - } - } + example: "Error getting DockStatAPI information", + }, + }, + }, + }, + }, + }, + }, }, }, ); From 911cb8582fe270fb64e68280d7bccbce234dd1e3 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 30 Apr 2025 17:08:49 +0000 Subject: [PATCH 288/369] CQL: Apply lint fixes [skip ci] --- src/core/utils/logger.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index e8b8404e..f9304ab1 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -71,11 +71,11 @@ const parseTimestamp = (timestamp: string): string => { const year = new Date().getFullYear(); const date = new Date( year, - parseInt(month) - 1, - parseInt(day), - parseInt(hours), - parseInt(minutes), - parseInt(seconds), + Number.parseInt(month) - 1, + Number.parseInt(day), + Number.parseInt(hours), + Number.parseInt(minutes), + Number.parseInt(seconds), ); return date.toISOString(); }; From 00e825361b76aafc0bc8cfd824478bccf49b1afb Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 30 Apr 2025 22:07:30 +0200 Subject: [PATCH 289/369] Feat: Eden treaty --- src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index badec71a..7b4ffbc3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,7 @@ console.log(""); logger.info("Starting DockStatAPI"); -export const DockStatAPI = new Elysia() +const DockStatAPI = new Elysia() .use(staticPlugin()) .use(serverTiming()) .use( @@ -176,3 +176,5 @@ await startServer(); logger.info("Started server"); console.log("----- [ ############## ]"); + +export type DockStatAPI = typeof DockStatAPI; From d0489582c7dcccfab3af49a2672ee414f342a534 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 30 Apr 2025 22:08:53 +0200 Subject: [PATCH 290/369] Fix: Export for ci --- src/index.ts | 276 +++++++++++++++++++++++++-------------------------- 1 file changed, 138 insertions(+), 138 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7b4ffbc3..2780d107 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,9 +9,9 @@ import { setSchedules } from "~/core/docker/scheduler"; import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { - authorWebsite, - contributors, - license, + authorWebsite, + contributors, + license, } from "~/core/utils/package-json"; import { swaggerReadme } from "~/core/utils/swagger-readme"; @@ -33,143 +33,143 @@ console.log(""); logger.info("Starting DockStatAPI"); -const DockStatAPI = new Elysia() - .use(staticPlugin()) - .use(serverTiming()) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey", - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }), - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; - - if ( - path === "/health" || - path.startsWith("/swagger") || - path.startsWith("/trpc") - ) { - logger.info(`Requested unguarded route: ${path}`); - return; - } - - const validation = await validateApiKey(request, set); - - if (validation.error) { - set.status = 400; - set.headers["Content-Type"] = "application/json"; - return { error: validation.error }; - } - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(utilRoutes) - .use(stackRoutes) - .use(utilRoutes) - .use(liveLogs) - .use(liveStacks) - .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) - .onError(({ code, set, path }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - }); +export const DockStatAPI = new Elysia() + .use(staticPlugin()) + .use(serverTiming()) + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "3.0.0", + description: swaggerReadme, + }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey", + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], + }, + }) + ) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; + + if ( + path === "/health" || + path.startsWith("/swagger") || + path.startsWith("/trpc") + ) { + logger.info(`Requested unguarded route: ${path}`); + return; + } + + const validation = await validateApiKey(request, set); + + if (validation.error) { + set.status = 400; + set.headers["Content-Type"] = "application/json"; + return { error: validation.error }; + } + }) + .use(dockerRoutes) + .use(dockerStatsRoutes) + .use(backendLogs) + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .use(utilRoutes) + .use(stackRoutes) + .use(utilRoutes) + .use(liveLogs) + .use(liveStacks) + .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) + .onError(({ code, set, path }) => { + if (code === "NOT_FOUND") { + logger.warn(`Unknown route (${path}), showing error page!`); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); + } + }); async function startServer() { - try { - try { - await loadPlugins("./src/plugins"); - } catch (error) { - throw new Error(`Failed to load plugins: ${error}`); - } - - try { - await setSchedules(); - } catch (error) { - throw new Error(`Failed to set schedules: ${error}`); - } - - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); - - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; - - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", - ); - } - - try { - DockStatAPI.listen( - process.env.DOCKSTATAPI_PORT || 3000, - ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }, - ); - } catch (error) { - logger.error("Failed to start server:", error); - process.exit(1); - } - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } + try { + try { + await loadPlugins("./src/plugins"); + } catch (error) { + throw new Error(`Failed to load plugins: ${error}`); + } + + try { + await setSchedules(); + } catch (error) { + throw new Error(`Failed to set schedules: ${error}`); + } + + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); + + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; + + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" + ); + } + + try { + DockStatAPI.listen( + process.env.DOCKSTATAPI_PORT || 3000, + ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger` + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + } + ); + } catch (error) { + logger.error("Failed to start server:", error); + process.exit(1); + } + } catch (error) { + logger.error("Error while starting server:", error); + process.exit(1); + } } await startServer(); From 6418d568a1de09904cc7b13cf1cc3174d1386ad6 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 30 Apr 2025 20:09:19 +0000 Subject: [PATCH 291/369] CQL: Apply lint fixes [skip ci] --- src/index.ts | 274 +++++++++++++++++++++++++-------------------------- 1 file changed, 137 insertions(+), 137 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2780d107..57dc887e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,9 +9,9 @@ import { setSchedules } from "~/core/docker/scheduler"; import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { - authorWebsite, - contributors, - license, + authorWebsite, + contributors, + license, } from "~/core/utils/package-json"; import { swaggerReadme } from "~/core/utils/swagger-readme"; @@ -34,142 +34,142 @@ console.log(""); logger.info("Starting DockStatAPI"); export const DockStatAPI = new Elysia() - .use(staticPlugin()) - .use(serverTiming()) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey", - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }) - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; - - if ( - path === "/health" || - path.startsWith("/swagger") || - path.startsWith("/trpc") - ) { - logger.info(`Requested unguarded route: ${path}`); - return; - } - - const validation = await validateApiKey(request, set); - - if (validation.error) { - set.status = 400; - set.headers["Content-Type"] = "application/json"; - return { error: validation.error }; - } - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(utilRoutes) - .use(stackRoutes) - .use(utilRoutes) - .use(liveLogs) - .use(liveStacks) - .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) - .onError(({ code, set, path }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - }); + .use(staticPlugin()) + .use(serverTiming()) + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "3.0.0", + description: swaggerReadme, + }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey", + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], + }, + }), + ) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; + + if ( + path === "/health" || + path.startsWith("/swagger") || + path.startsWith("/trpc") + ) { + logger.info(`Requested unguarded route: ${path}`); + return; + } + + const validation = await validateApiKey(request, set); + + if (validation.error) { + set.status = 400; + set.headers["Content-Type"] = "application/json"; + return { error: validation.error }; + } + }) + .use(dockerRoutes) + .use(dockerStatsRoutes) + .use(backendLogs) + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .use(utilRoutes) + .use(stackRoutes) + .use(utilRoutes) + .use(liveLogs) + .use(liveStacks) + .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) + .onError(({ code, set, path }) => { + if (code === "NOT_FOUND") { + logger.warn(`Unknown route (${path}), showing error page!`); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); + } + }); async function startServer() { - try { - try { - await loadPlugins("./src/plugins"); - } catch (error) { - throw new Error(`Failed to load plugins: ${error}`); - } - - try { - await setSchedules(); - } catch (error) { - throw new Error(`Failed to set schedules: ${error}`); - } - - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); - - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; - - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" - ); - } - - try { - DockStatAPI.listen( - process.env.DOCKSTATAPI_PORT || 3000, - ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger` - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - } - ); - } catch (error) { - logger.error("Failed to start server:", error); - process.exit(1); - } - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } + try { + try { + await loadPlugins("./src/plugins"); + } catch (error) { + throw new Error(`Failed to load plugins: ${error}`); + } + + try { + await setSchedules(); + } catch (error) { + throw new Error(`Failed to set schedules: ${error}`); + } + + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); + + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; + + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", + ); + } + + try { + DockStatAPI.listen( + process.env.DOCKSTATAPI_PORT || 3000, + ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + }, + ); + } catch (error) { + logger.error("Failed to start server:", error); + process.exit(1); + } + } catch (error) { + logger.error("Error while starting server:", error); + process.exit(1); + } } await startServer(); From 10a1921c8d16aa5fe6d43803af6bb4ce674fbfe0 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 3 May 2025 08:27:29 +0200 Subject: [PATCH 292/369] Fix: Formatting --- bun.lock | 420 ------------ package.json | 108 +-- src/core/utils/calculations.ts | 4 +- src/index.js | 1 + src/index.ts | 280 ++++---- src/routes/api-config.ts | 1145 ++++++++++++++++---------------- src/routes/docker-stats.ts | 772 ++++++++++++--------- tsconfig.json | 2 +- 8 files changed, 1236 insertions(+), 1496 deletions(-) delete mode 100644 bun.lock create mode 100644 src/index.js diff --git a/bun.lock b/bun.lock deleted file mode 100644 index ad696a27..00000000 --- a/bun.lock +++ /dev/null @@ -1,420 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "dockstatapi", - "dependencies": { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - "@elysiajs/swagger": "^1.2.2", - "chalk": "^5.4.1", - "docker-compose": "^1.2.0", - "dockerode": "^4.0.5", - "elysia": "latest", - "knip": "latest", - "split2": "^4.2.0", - "winston": "^3.17.0", - "yaml": "^2.7.1", - }, - "devDependencies": { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - "@types/node": "^22.14.1", - "@types/split2": "^4.2.3", - "bun-types": "latest", - "cross-env": "^7.0.3", - "logform": "^2.7.0", - "typescript": "^5.8.3", - "wrap-ansi": "^9.0.0", - }, - }, - }, - "trustedDependencies": [ - "protobufjs", - ], - "packages": { - "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], - - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], - - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], - - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], - - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], - - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], - - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], - - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], - - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], - - "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], - - "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], - - "@elysiajs/server-timing": ["@elysiajs/server-timing@1.2.1", "", { "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-7i4xOSYRdgljxKg8fyyBPVtnwsjhvJBnJn4qpTiNXt6ElrW1V2FeV2rdhyw2AQagUknnfpbUXMeDLalPaDeaLQ=="], - - "@elysiajs/static": ["@elysiajs/static@1.2.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-oLpAi8c+maPpA0XhhK3BELaIjIG+nXg/K9p8cFfW4q5ayRD59a3MOMOOGgpiXZkHJzLPWcouhhyyLAYtaANW4g=="], - - "@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="], - - "@grpc/grpc-js": ["@grpc/grpc-js@1.13.3", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg=="], - - "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], - - "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], - - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], - - "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], - - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], - - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], - - "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - - "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], - - "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], - - "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], - - "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], - - "@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="], - - "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], - - "@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="], - - "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], - - "@types/dockerode": ["@types/dockerode@3.3.38", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-nnrcfUe2iR+RyOuz0B4bZgQwD9djQa9ADEjp7OAgBs10pYT0KSCtplJjcmBDJz0qaReX5T7GbE5i4VplvzUHvA=="], - - "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="], - - "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], - - "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], - - "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], - - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - - "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], - - "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - - "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], - - "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], - - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - - "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], - - "bun-types": ["bun-types@1.2.9", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw=="], - - "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], - - "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], - - "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - - "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], - - "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], - - "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], - - "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - - "docker-compose": ["docker-compose@1.2.0", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-wIU1eHk3Op7dFgELRdmOYlPYS4gP8HhH1ZmZa13QZF59y0fblzFDFmKPhyc05phCy2hze9OEvNZAsoljrs+72w=="], - - "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], - - "dockerode": ["dockerode@4.0.5", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.1.2", "uuid": "^10.0.0" } }, "sha512-ZPmKSr1k1571Mrh7oIBS/j0AqAccoecY2yH420ni5j1KyNMgnoTh4Nu4FWunh0HZIJmRSmSysJjBIpa/zyWUEA=="], - - "easy-table": ["easy-table@1.2.0", "", { "dependencies": { "ansi-regex": "^5.0.1" }, "optionalDependencies": { "wcwidth": "^1.0.1" } }, "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww=="], - - "elysia": ["elysia@1.2.25", "", { "dependencies": { "@sinclair/typebox": "^0.34.27", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-WsdQpORJvb4uszzeqYT0lg97knw1iBW1NTzJ1Jm57tiHg+DfAotlWXYbjmvQ039ssV0fYELDHinLLoUazZkEHg=="], - - "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], - - "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], - - "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], - - "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], - - "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], - - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], - - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], - - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], - - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - - "knip": ["knip@5.50.4", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "easy-table": "1.2.0", "enhanced-resolve": "^5.18.1", "fast-glob": "^3.3.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "pretty-ms": "^9.0.0", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-In+GjPpd2P3IDZnBBP4QF27vhQOhuBkICiuN9j+DMOf/m/qAFLGcbvuAGxco8IDvf26pvBnfeSmm1f6iNCkgOA=="], - - "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], - - "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], - - "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], - - "long": ["long@5.3.1", "", {}, "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng=="], - - "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="], - - "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], - - "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], - - "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - - "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], - - "protobufjs": ["protobufjs@7.5.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-Z2E/kOY1QjoMlCytmexzYfDm/w5fKAiRwpSzGtdnXW1zC88Z2yXazHHrOtwCzn+7wSxyE8PYM4rvVcMphF9sOA=="], - - "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], - - "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="], - - "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], - - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - - "ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="], - - "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], - - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - - "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="], - - "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], - - "tar-fs": ["tar-fs@2.1.2", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA=="], - - "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - - "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], - - "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], - - "type-fest": ["type-fest@4.40.0", "", {}, "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw=="], - - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], - - "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], - - "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - - "yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], - - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], - - "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], - - "zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="], - - "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], - - "@types/ssh2/@types/node": ["@types/node@18.19.86", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ=="], - - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - - "easy-table/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], - - "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - } -} diff --git a/package.json b/package.json index a3e18f4f..a13dd824 100644 --- a/package.json +++ b/package.json @@ -1,52 +1,58 @@ { - "name": "dockstatapi", - "author": { - "email": "info@itsnik.de", - "name": "ItsNik", - "url": "https://github.com/Its4Nik" - }, - "license": "CC BY-NC 4.0", - "contributors": [], - "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", - "version": "3.0.0", - "scripts": { - "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", - "start:docker": "bun run build:docker && docker run -p 3000:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", - "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", - "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", - "build": "bun build --target bun src/index.ts --outdir ./dist", - "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", - "clean": "bun run clean:win || bun run clean:lin", - "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi*.db* && echo 'success'", - "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi*.db* && echo 'success'", - "knip": "knip", - "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src", - "test": "bun test src/tests/**/*.test.ts" - }, - "dependencies": { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - "@elysiajs/swagger": "^1.2.2", - "chalk": "^5.4.1", - "docker-compose": "^1.2.0", - "dockerode": "^4.0.5", - "elysia": "latest", - "knip": "latest", - "split2": "^4.2.0", - "winston": "^3.17.0", - "yaml": "^2.7.1" - }, - "devDependencies": { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - "@types/node": "^22.14.1", - "@types/split2": "^4.2.3", - "bun-types": "latest", - "cross-env": "^7.0.3", - "logform": "^2.7.0", - "typescript": "^5.8.3", - "wrap-ansi": "^9.0.0" - }, - "module": "src/index.js", - "trustedDependencies": ["protobufjs"] -} + "name": "dockstatapi", + "author": { + "email": "info@itsnik.de", + "name": "ItsNik", + "url": "https://github.com/Its4Nik" + }, + "license": "CC BY-NC 4.0", + "contributors": [], + "description": "DockStatAPI is an API backend featuring plugins and more for DockStat", + "version": "3.0.0", + "scripts": { + "start": "cross-env NODE_ENV=production LOG_LEVEL=info bun run src/index.ts", + "start:docker": "bun run build:docker && docker run -p 3000:3000 --rm -d --name dockstatapi -v 'plugins:/DockStatAPI/src/plugins' dockstatapi:local", + "dev": "docker compose -f docker/docker-compose.dev.yaml up -d && cross-env NODE_ENV=dev bun run --watch src/index.ts", + "dev:clean": "bun dev ; echo '\nExiting...' ; bun clean", + "build": "bun build --target bun src/index.ts --outdir ./dist", + "build:prod": "NODE_ENV=production bun build --no-native --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts", + "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", + "clean": "bun run clean:win || bun run clean:lin", + "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi*.db* && echo 'success'", + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi*.db* && echo 'success'", + "knip": "knip", + "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src", + "test": "bun test src/tests/**/*.test.ts" + }, + "dependencies": { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + "@elysiajs/swagger": "^1.2.2", + "chalk": "^5.4.1", + "docker-compose": "^1.2.0", + "dockerode": "^4.0.6", + "elysia": "latest", + "knip": "latest", + "split2": "^4.2.0", + "winston": "^3.17.0", + "yaml": "^2.7.1" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + "@types/node": "^22.15.3", + "@types/split2": "^4.2.3", + "bun-types": "latest", + "cross-env": "^7.0.3", + "logform": "^2.7.0", + "typescript": "^5.8.3", + "wrap-ansi": "^9.0.0", + "@types/bun": "latest" + }, + "module": "src/index.js", + "trustedDependencies": [ + "protobufjs" + ], + "type": "module", + "private": true +} \ No newline at end of file diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index 1b5c8930..b640c47a 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -21,7 +21,7 @@ const calculateCpuPercent = (stats: Docker.ContainerStats): number => { return 0.0000001; } - return data; + return data * 10; }; const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { @@ -31,7 +31,7 @@ const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; - return data; + return data ; }; export { calculateCpuPercent, calculateMemoryUsage }; diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..f67b2c64 --- /dev/null +++ b/src/index.js @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 57dc887e..a9d72d8b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; import { swagger } from "@elysiajs/swagger"; -import { Elysia } from "elysia"; +import { Elysia, t } from "elysia"; import { dbFunctions } from "~/core/database"; import { monitorDockerEvents } from "~/core/docker/monitor"; @@ -9,9 +9,9 @@ import { setSchedules } from "~/core/docker/scheduler"; import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { - authorWebsite, - contributors, - license, + authorWebsite, + contributors, + license, } from "~/core/utils/package-json"; import { swaggerReadme } from "~/core/utils/swagger-readme"; @@ -33,148 +33,130 @@ console.log(""); logger.info("Starting DockStatAPI"); -export const DockStatAPI = new Elysia() - .use(staticPlugin()) - .use(serverTiming()) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey", - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }), - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; - - if ( - path === "/health" || - path.startsWith("/swagger") || - path.startsWith("/trpc") - ) { - logger.info(`Requested unguarded route: ${path}`); - return; - } - - const validation = await validateApiKey(request, set); - - if (validation.error) { - set.status = 400; - set.headers["Content-Type"] = "application/json"; - return { error: validation.error }; - } - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(utilRoutes) - .use(stackRoutes) - .use(utilRoutes) - .use(liveLogs) - .use(liveStacks) - .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) - .onError(({ code, set, path }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - }); - -async function startServer() { - try { - try { - await loadPlugins("./src/plugins"); - } catch (error) { - throw new Error(`Failed to load plugins: ${error}`); - } - - try { - await setSchedules(); - } catch (error) { - throw new Error(`Failed to set schedules: ${error}`); - } - - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); - - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; - - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", - ); - } - - try { - DockStatAPI.listen( - process.env.DOCKSTATAPI_PORT || 3000, - ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }, - ); - } catch (error) { - logger.error("Failed to start server:", error); - process.exit(1); - } - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } -} - -await startServer(); - -logger.info("Started server"); -console.log("----- [ ############## ]"); - -export type DockStatAPI = typeof DockStatAPI; +const DockStatAPI = new Elysia() + .use(staticPlugin()) + .use(serverTiming()) + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "3.0.0", + description: swaggerReadme, + }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey" as const, + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], + }, + }) + ) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; + + if ( + path === "/health" || + path.startsWith("/swagger") || + path.startsWith("/trpc") + ) { + logger.info(`Requested unguarded route: ${path}`); + return; + } + + const validation = await validateApiKey(request, set); + + if (validation.error) { + set.status = 400; + set.headers["Content-Type"] = "application/json"; + return { error: validation.error }; + } + }) + .onError(({ code, set, path }) => { + if (code === "NOT_FOUND") { + logger.warn(`Unknown route (${path}), showing error page!`); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); + } + }) + .use(dockerRoutes) + .use(dockerStatsRoutes) + .use(backendLogs) + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .use(utilRoutes) + .use(stackRoutes) + .use(liveLogs) + .use(liveStacks) + .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) + .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger` + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + }); + +const initializeServer = async () => { + try { + await loadPlugins("./src/plugins"); + await setSchedules(); + + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); + + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; + + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" + ); + } + + logger.info("Started server"); + console.log("----- [ ############## ]"); + } catch (error) { + logger.error("Error while starting server:", error); + process.exit(1); + } +}; + +await initializeServer(); + +export { DockStatAPI }; +export type App = typeof DockStatAPI; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 5ccca3e5..79749e6f 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -1,18 +1,18 @@ -import { existsSync, readdir, readdirSync, unlinkSync } from "node:fs"; +import { existsSync, readdirSync, unlinkSync } from "node:fs"; import { Elysia, t } from "elysia"; import { dbFunctions } from "~/core/database"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import { responseHandler } from "~/core/utils/response-handler"; @@ -21,576 +21,577 @@ import { hashApiKey } from "~/middleware/auth"; import type { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "/", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; - set.headers["Content-Type"] = "application/json"; - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting the DockStatAPI config", - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - responses: { - "200": { - description: "Successfully retrieved configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - fetching_interval: { - type: "number", - example: 5, - }, - keep_data_for: { - type: "number", - example: 7, - }, - api_key: { - type: "string", - example: "hashed_api_key", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/plugins", - ({ set }) => { - try { - return pluginManager.getLoadedPlugins(); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting all registered plugins", - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - responses: { - "200": { - description: "Successfully retrieved plugins", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-plugin", - }, - version: { - type: "string", - example: "1.0.0", - }, - status: { - type: "string", - example: "active", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving plugins", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting all registered plugins", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; - set.headers["Content-Type"] = "application/json"; - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key), - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - return responseHandler.error( - set, - "Error updating the DockStatAPI config", - error as string, - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - responses: { - "200": { - description: "Successfully updated configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated DockStatAPI config", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error updating the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - }, - ) - .get( - "/package", - async ({ set }) => { - try { - logger.debug("Fetching package.json"); - return { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error while reading package.json", - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - responses: { - "200": { - description: "Successfully retrieved package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error while reading package.json", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .post( - "/backup", - async ({ set }) => { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return responseHandler.ok(set, backupFilename); - } catch (error) { - return responseHandler.error(set, error as string, "Error backing up"); - } - }, - { - detail: { - tags: ["Management"], - description: "Backs up the internal database", - responses: { - "200": { - description: "Successfully created backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "backup_2024-03-20_12-00-00.db.bak", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error creating backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error backing up", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/backup", - async ({ set }) => { - try { - const backupFiles = readdirSync(backupDir); + .get( + "/", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; + set.headers["Content-Type"] = "application/json"; + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error getting the DockStatAPI config" + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Returns current API configuration including data retention policies and security settings", + responses: { + "200": { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fetching_interval: { + type: "number", + example: 5, + }, + keep_data_for: { + type: "number", + example: 7, + }, + api_key: { + type: "string", + example: "hashed_api_key", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/plugins", + ({ set }) => { + try { + return pluginManager.getLoadedPlugins(); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error getting all registered plugins" + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + responses: { + "200": { + description: "Successfully retrieved plugins", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-plugin", + }, + version: { + type: "string", + example: "1.0.0", + }, + status: { + type: "string", + example: "active", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving plugins", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting all registered plugins", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { fetching_interval, keep_data_for, api_key } = body; + set.headers["Content-Type"] = "application/json"; + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key) + ); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + return responseHandler.error( + set, + "Error updating the DockStatAPI config", + error as string + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", + responses: { + "200": { + description: "Successfully updated configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated DockStatAPI config", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error updating the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), + } + ) + .get( + "/package", + async ({ set }) => { + try { + logger.debug("Fetching package.json"); + return { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error while reading package.json" + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Displays package metadata including dependencies, contributors, and licensing information", + responses: { + "200": { + description: "Successfully retrieved package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0", + }, + description: { + type: "string", + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", + }, + license: { + type: "string", + example: "CC BY-NC 4.0", + }, + authorName: { + type: "string", + example: "ItsNik", + }, + authorEmail: { + type: "string", + example: "info@itsnik.de", + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik", + }, + contributors: { + type: "array", + items: { + type: "string", + }, + example: [], + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + }, + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error while reading package.json", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .post( + "/backup", + async ({ set }) => { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return responseHandler.ok(set, backupFilename); + } catch (error) { + return responseHandler.error(set, error as string, "Error backing up"); + } + }, + { + detail: { + tags: ["Management"], + description: "Backs up the internal database", + responses: { + "200": { + description: "Successfully created backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "backup_2024-03-20_12-00-00.db.bak", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error creating backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error backing up", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/backup", + async ({ set }) => { + try { + const backupFiles = readdirSync(backupDir); - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); - return filteredFiles; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Reading Backup directory", - ); - } - }, - { - detail: { - tags: ["Management"], - description: "Lists all available backups", - responses: { - "200": { - description: "Successfully retrieved backup list", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "string", - }, - example: [ - "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak", - ], - }, - }, - }, - }, - "400": { - description: "Error retrieving backup list", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Reading Backup directory", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) + return filteredFiles; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Reading Backup directory" + ); + } + }, + { + detail: { + tags: ["Management"], + description: "Lists all available backups", + responses: { + "200": { + description: "Successfully retrieved backup list", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "string", + }, + example: [ + "backup_2024-03-20_12-00-00.db.bak", + "backup_2024-03-19_12-00-00.db.bak", + ], + }, + }, + }, + }, + "400": { + description: "Error retrieving backup list", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Reading Backup directory", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) - .get( - "/backup/download", - async ({ query, set }) => { - try { - const filename = query.filename || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; + .get( + "/backup/download", + async ({ query, set }) => { + try { + const filename = query.filename || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } - set.headers["Content-Type"] = "application/octet-stream"; - set.headers["Content-Disposition"] = - `attachment; filename="${filename}"`; - return Bun.file(filePath); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Backup download failed", - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Download a specific backup or the latest if no filename is provided", - responses: { - "200": { - description: "Successfully downloaded backup file", - content: { - "application/octet-stream": { - schema: { - type: "string", - format: "binary", - example: "Binary backup file content", - }, - }, - }, - headers: { - "Content-Disposition": { - schema: { - type: "string", - example: - 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', - }, - }, - }, - }, - "400": { - description: "Error downloading backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Backup download failed", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - filename: t.Optional(t.String()), - }), - }, - ) - .post( - "/restore", - async ({ body, set }) => { - try { - const { file } = body; + set.headers["Content-Type"] = "application/octet-stream"; + set.headers[ + "Content-Disposition" + ] = `attachment; filename="${filename}"`; + return Bun.file(filePath); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Backup download failed" + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Download a specific backup or the latest if no filename is provided", + responses: { + "200": { + description: "Successfully downloaded backup file", + content: { + "application/octet-stream": { + schema: { + type: "string", + format: "binary", + example: "Binary backup file content", + }, + }, + }, + headers: { + "Content-Disposition": { + schema: { + type: "string", + example: + 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', + }, + }, + }, + }, + "400": { + description: "Error downloading backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Backup download failed", + }, + }, + }, + }, + }, + }, + }, + }, + query: t.Object({ + filename: t.Optional(t.String()), + }), + } + ) + .post( + "/restore", + async ({ body, set }) => { + try { + const { file } = body; - set.headers["Content-Type"] = "text/html"; + set.headers["Content-Type"] = "text/html"; - if (!file) { - throw new Error("No file uploaded"); - } + if (!file) { + throw new Error("No file uploaded"); + } - if (!file.name.endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } + if (!file.name.endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); - return responseHandler.ok(set, "Database restored successfully"); - } catch (error) { - return responseHandler.error( - set, - error instanceof Error ? error.message : "Restoration failed", - "Database restoration error", - ); - } - }, - { - body: t.Object({ file: t.File() }), - detail: { - tags: ["Management"], - description: "Restore database from uploaded backup file", - responses: { - "200": { - description: "Successfully restored database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Database restored successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restoring database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Database restoration error", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); + return responseHandler.ok(set, "Database restored successfully"); + } catch (error) { + return responseHandler.error( + set, + error instanceof Error ? error.message : "Restoration failed", + "Database restoration error" + ); + } + }, + { + body: t.Object({ file: t.File() }), + detail: { + tags: ["Management"], + description: "Restore database from uploaded backup file", + responses: { + "200": { + description: "Successfully restored database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Database restored successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error restoring database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Database restoration error", + }, + }, + }, + }, + }, + }, + }, + }, + } + ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 3781f26b..42b887d3 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -4,8 +4,8 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; @@ -15,311 +15,481 @@ import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get( - "/containers", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; + .get( + "/containers", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - return responseHandler.error( - set, - pingError as string, - "Docker host connection failed", - ); - } + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + return responseHandler.error( + set, + pingError as string, + "Docker host connection failed" + ); + } - const hostContainers = await docker.listContainers({ all: true }); + const hostContainers = await docker.listContainers({ all: true }); - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - return responseHandler.reject( - set, - reject, - "An error occurred", - error, - ); - } - if (!stats) { - return responseHandler.reject( - set, - reject, - "No stats available", - ); - } - resolve(stats); - }); - }, - ); + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return responseHandler.reject( + set, + reject, + "An error occurred", + error + ); + } + if (!stats) { + return responseHandler.reject( + set, + reject, + "No stats available" + ); + } + resolve(stats); + }); + } + ); - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError, - ); - } - }), - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (hostError) { - logger.error("Error fetching containers for host,", hostError); - } - }), - ); + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError + ); + } + }) + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (hostError) { + logger.error("Error fetching containers for host,", hostError); + } + }) + ); - set.headers["Content-Type"] = "application/json"; - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve containers", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", - responses: { - "200": { - description: "Successfully retrieved container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - containers: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "string", - example: "abc123def456", - }, - hostId: { - type: "string", - example: "1", - }, - name: { - type: "string", - example: "example-container", - }, - image: { - type: "string", - example: "nginx:latest", - }, - status: { - type: "string", - example: "running", - }, - state: { - type: "string", - example: "running", - }, - cpuUsage: { - type: "number", - example: 0.5, - }, - memoryUsage: { - type: "number", - example: 1024, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve containers", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) + set.headers["Content-Type"] = "application/json"; + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve containers" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", + responses: { + "200": { + description: "Successfully retrieved container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + containers: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + example: "abc123def456", + }, + hostId: { + type: "string", + example: "1", + }, + name: { + type: "string", + example: "example-container", + }, + image: { + type: "string", + example: "nginx:latest", + }, + status: { + type: "string", + example: "running", + }, + state: { + type: "string", + example: "running", + }, + cpuUsage: { + type: "number", + example: 0.5, + }, + memoryUsage: { + type: "number", + example: 1024, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve containers", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - .get( - "/hosts/:id", - async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = findObjectByKey(hosts, "name", params.id); - if (!host) { - return responseHandler.simple_error( - set, - `Host (${params.id}) not found`, - ); - } + const stats: HostStats[] = []; - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - set.headers["Content-Type"] = "application/json"; - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); + stats.push(config); + } + + set.headers["Content-Type"] = "application/json"; + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/hosts/:id", + async ({ params, set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + if (!params.id) { + const stats: HostStats[] = []; + + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + stats.push(config); + } + + return stats; + } + + const host = findObjectByKey(hosts, "id", Number(params.id)); + if (!host) { + return responseHandler.simple_error( + set, + `Host (${params.id}) not found` + ); + } + + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + set.headers["Content-Type"] = "application/json"; + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ); diff --git a/tsconfig.json b/tsconfig.json index 3a44e369..fc07b9ba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ /* Projects */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ From 595002baedb90f2a26ab68c39ddfb428c3710c7c Mon Sep 17 00:00:00 2001 From: ItsNik Date: Sat, 3 May 2025 08:27:59 +0200 Subject: [PATCH 293/369] Fix: Remove index.js --- src/index.js | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/index.js diff --git a/src/index.js b/src/index.js deleted file mode 100644 index f67b2c64..00000000 --- a/src/index.js +++ /dev/null @@ -1 +0,0 @@ -console.log("Hello via Bun!"); \ No newline at end of file From b561168aa69a3d40e7598822acd1b1548a617ac9 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 3 May 2025 06:28:02 +0000 Subject: [PATCH 294/369] Update dependency graphs --- dependency-graph.mmd | 208 ++++--- dependency-graph.svg | 1339 +++++++++++++++++++++--------------------- 2 files changed, 765 insertions(+), 782 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index dcab5575..1920bf8b 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -11,23 +11,23 @@ subgraph 0["src"] subgraph 5["routes"] 6["live-stacks.ts"] U["live-logs.ts"] -1H["api-config.ts"] -1J["docker-manager.ts"] -1K["docker-stats.ts"] -1L["docker-websocket.ts"] -1N["logs.ts"] -1O["stacks.ts"] -1R["utils.ts"] +1G["api-config.ts"] +1I["docker-manager.ts"] +1J["docker-stats.ts"] +1K["docker-websocket.ts"] +1M["logs.ts"] +1N["stacks.ts"] +1Q["utils.ts"] end subgraph 8["core"] subgraph 9["utils"] A["logger.ts"] T["helpers.ts"] -15["calculations.ts"] -19["change-me-checker.ts"] -1B["package-json.ts"] -1D["swagger-readme.ts"] -1I["response-handler.ts"] +14["calculations.ts"] +18["change-me-checker.ts"] +1A["package-json.ts"] +1C["swagger-readme.ts"] +1H["response-handler.ts"] end subgraph C["database"] D["_dbState.ts"] @@ -44,21 +44,21 @@ R["stacks.ts"] end subgraph V["docker"] W["monitor.ts"] -12["client.ts"] -13["scheduler.ts"] -14["store-container-stats.ts"] -16["store-host-stats.ts"] +11["client.ts"] +12["scheduler.ts"] +13["store-container-stats.ts"] +15["store-host-stats.ts"] end -subgraph Y["plugins"] -Z["plugin-manager.ts"] -18["loader.ts"] +subgraph X["plugins"] +Y["plugin-manager.ts"] +17["loader.ts"] end -subgraph 1P["stacks"] -1Q["controller.ts"] +subgraph 1O["stacks"] +1P["controller.ts"] end end -subgraph 1E["middleware"] -1F["auth.ts"] +subgraph 1D["middleware"] +1E["auth.ts"] end end subgraph 2["~"] @@ -68,37 +68,36 @@ subgraph 3["typings"] G["misc"] O["docker"] S["docker-compose"] -10["plugin"] -17["dockerode"] -1G["elysiajs"] +Z["plugin"] +16["dockerode"] +1F["elysiajs"] end end B["path"] subgraph H["fs"] -1A["promises"] +19["promises"] end J["bun:sqlite"] -X["bun"] -11["events"] -1C["package.json"] -1M["stream"] +10["events"] +1B["package.json"] +1L["stream"] 1-->6 1-->E 1-->W -1-->13 -1-->18 +1-->12 +1-->17 1-->A -1-->1B -1-->1D -1-->1F -1-->1H +1-->1A +1-->1C +1-->1E +1-->1G +1-->1I 1-->1J 1-->1K -1-->1L 1-->U +1-->1M 1-->1N -1-->1O -1-->1R +1-->1Q 1-->4 6-->A 6-->7 @@ -146,87 +145,86 @@ R-->S T-->A U-->A U-->4 -W-->Z +W-->Y W-->E -W-->12 +W-->11 W-->A W-->O -W-->X -Z-->A -Z-->O -Z-->10 -Z-->11 +Y-->A +Y-->O +Y-->Z +Y-->10 +11-->A +11-->O +12-->E +12-->13 +12-->15 12-->A -12-->O +12-->4 +13-->A 13-->E +13-->11 13-->14 -13-->16 -13-->A -13-->4 -14-->A -14-->E -14-->12 -14-->15 -16-->E -16-->12 -16-->T -16-->A -16-->O -16-->17 -18-->19 +15-->E +15-->11 +15-->T +15-->A +15-->O +15-->16 +17-->18 +17-->A +17-->Y +17-->H +17-->B 18-->A -18-->Z -18-->H -18-->B -19-->A -19-->1A -1B-->1C -1F-->E -1F-->A -1F-->4 -1F-->1G -1H-->E -1H-->F -1H-->Z +18-->19 +1A-->1B +1E-->E +1E-->A +1E-->4 +1E-->1F +1G-->E +1G-->F +1G-->Y +1G-->A +1G-->1A +1G-->1H +1G-->1E +1G-->4 +1G-->H 1H-->A -1H-->1B -1H-->1I 1H-->1F -1H-->4 -1H-->H +1I-->E 1I-->A -1I-->1G +1I-->1H +1I-->O 1J-->E +1J-->11 +1J-->14 +1J-->T 1J-->A -1J-->1I +1J-->1H 1J-->O +1J-->16 1K-->E -1K-->12 -1K-->15 -1K-->T +1K-->11 +1K-->14 1K-->A -1K-->1I -1K-->O -1K-->17 -1L-->E -1L-->12 -1L-->15 -1L-->A -1L-->1I -1L-->1M +1K-->1H +1K-->1L +1M-->E +1M-->A 1N-->E +1N-->1P 1N-->A -1O-->E -1O-->1Q -1O-->A -1O-->1I -1Q-->T -1Q-->E -1Q-->A -1Q-->6 -1Q-->4 -1Q-->S +1N-->1H +1P-->T +1P-->E +1P-->A +1P-->6 +1P-->4 +1P-->S +1P-->19 1Q-->1A -1R-->1B -1R-->1I +1Q-->1H diff --git a/dependency-graph.svg b/dependency-graph.svg index 7a12bb42..092821e9 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,60 +4,60 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_~ @@ -69,1406 +69,1391 @@ typings - - -bun - - -bun - - - - + bun:sqlite - - -bun:sqlite + + +bun:sqlite - + events - - -events + + +events - + fs - - -fs + + +fs - + fs/promises - - -promises + + +promises - + package.json - - -package.json + + +package.json - + path - - -path + + +path - + src/core/database/_dbState.ts - - -_dbState.ts + + +_dbState.ts - + src/core/database/backup.ts - - -backup.ts + + +backup.ts src/core/database/backup.ts->fs - - + + src/core/database/backup.ts->src/core/database/_dbState.ts - - + + - + src/core/database/database.ts - - -database.ts + + +database.ts src/core/database/backup.ts->src/core/database/database.ts - - + + - + src/core/database/helper.ts - - -helper.ts + + +helper.ts src/core/database/backup.ts->src/core/database/helper.ts - - - - + + + + - + src/core/utils/logger.ts - - -logger.ts + + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + - + ~/typings/misc - - -misc + + +misc src/core/database/backup.ts->~/typings/misc - - + + src/core/database/database.ts->bun:sqlite - - + + src/core/database/database.ts->fs - - + + src/core/database/helper.ts->src/core/database/_dbState.ts - - + + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + - + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + - + src/core/database/index.ts - - -index.ts + + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + - + ~/typings/database - + database - + src/core/utils/logger.ts->~/typings/database - - + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + - + src/core/database/config.ts - - -config.ts + + +config.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/containerStats.ts - - -containerStats.ts + + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/dockerHosts.ts - - -dockerHosts.ts + + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + - + ~/typings/docker - - -docker + + +docker src/core/database/dockerHosts.ts->~/typings/docker - - + + - + src/core/database/hostStats.ts - - -hostStats.ts + + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts->~/typings/docker - - + + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + - + src/core/database/logs.ts - - -logs.ts + + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + - + src/core/database/stacks.ts - - -stacks.ts + + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + src/core/database/logs.ts->~/typings/database - - + + src/core/database/stacks.ts->src/core/database/database.ts - - + + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + src/core/database/stacks.ts->~/typings/database - - + + - + src/core/utils/helpers.ts - - -helpers.ts + + +helpers.ts src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - + + + + - + ~/typings/docker-compose - - -docker-compose + + +docker-compose src/core/database/stacks.ts->~/typings/docker-compose - - + + - + src/core/utils/helpers.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/docker/client.ts - - -client.ts + + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->~/typings/docker - - + + - + src/core/docker/monitor.ts - - -monitor.ts + + +monitor.ts - - -src/core/docker/monitor.ts->bun - - - src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->~/typings/docker - - + + src/core/docker/monitor.ts->src/core/database/index.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + - + src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->~/typings/docker - - + + - + ~/typings/plugin - - -plugin + + +plugin - + src/core/plugins/plugin-manager.ts->~/typings/plugin - - + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + - + src/core/docker/scheduler.ts->~/typings/database - - + + - + src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - - -calculations.ts + + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-host-stats.ts->~/typings/docker - - + + - + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + ~/typings/dockerode - + dockerode - + src/core/docker/store-host-stats.ts->~/typings/dockerode - - + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts->~/typings/database - - + + - + src/core/stacks/controller.ts->src/core/utils/helpers.ts - - + + - + src/core/stacks/controller.ts->~/typings/docker-compose - - + + - + src/routes/live-stacks.ts - - -live-stacks.ts + + +live-stacks.ts - + src/core/stacks/controller.ts->src/routes/live-stacks.ts - - + + - + src/routes/live-stacks.ts->src/core/utils/logger.ts - - + + - + ~/typings/websocket - + websocket - + src/routes/live-stacks.ts->~/typings/websocket - + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->~/typings/database - - + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + ~/typings/elysiajs - - -elysiajs + + +elysiajs - + src/core/utils/response-handler.ts->~/typings/elysiajs - - + + - + src/core/utils/swagger-readme.ts - - -swagger-readme.ts + + +swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->~/typings/database - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/routes/live-stacks.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + - + src/index.ts->src/core/utils/package-json.ts - - + + - + src/index.ts->src/core/utils/swagger-readme.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/utils.ts - - -utils.ts + + +utils.ts - + src/index.ts->src/routes/utils.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->~/typings/database - - + + - + src/middleware/auth.ts->~/typings/elysiajs - - + + - + src/routes/api-config.ts->fs - - + + - + src/routes/api-config.ts->src/core/database/backup.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->~/typings/database - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->~/typings/docker - - + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->~/typings/docker - - + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/helpers.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->~/typings/dockerode - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + - + src/routes/utils.ts->src/core/utils/package-json.ts - - + + - + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + From 2bfdcce62d679b4e93194d1c2af381af3ff45c97 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 6 May 2025 20:13:41 +0200 Subject: [PATCH 295/369] Feat: Elysia server.d.ts route --- package.json | 1 + src/core/utils/swagger-readme.ts | 2 ++ src/index.ts | 15 ++++++++++----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a13dd824..3217c54d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "docker-compose": "^1.2.0", "dockerode": "^4.0.6", "elysia": "latest", + "elysia-remote-dts": "^1.0.2", "knip": "latest", "split2": "^4.2.0", "winston": "^3.17.0", diff --git a/src/core/utils/swagger-readme.ts b/src/core/utils/swagger-readme.ts index ff30a8bf..c1457c68 100644 --- a/src/core/utils/swagger-readme.ts +++ b/src/core/utils/swagger-readme.ts @@ -1,4 +1,6 @@ export const swaggerReadme: string = ` +[Download API type sheet](/server.d.ts) + ![Docker](https://img.shields.io/badge/Docker-2CA5E0?style=flat&logo=docker&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white) diff --git a/src/index.ts b/src/index.ts index a9d72d8b..e2d478ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; import { swagger } from "@elysiajs/swagger"; import { Elysia, t } from "elysia"; - +import { dts } from "elysia-remote-dts"; import { dbFunctions } from "~/core/database"; import { monitorDockerEvents } from "~/core/docker/monitor"; import { setSchedules } from "~/core/docker/scheduler"; @@ -14,9 +14,7 @@ import { license, } from "~/core/utils/package-json"; import { swaggerReadme } from "~/core/utils/swagger-readme"; - import { validateApiKey } from "~/middleware/auth"; - import { apiConfigRoutes } from "~/routes/api-config"; import { dockerRoutes } from "~/routes/docker-manager"; import { dockerStatsRoutes } from "~/routes/docker-stats"; @@ -25,9 +23,8 @@ import { liveLogs } from "~/routes/live-logs"; import { backendLogs } from "~/routes/logs"; import { stackRoutes } from "~/routes/stacks"; import { utilRoutes } from "~/routes/utils"; -import { liveStacks } from "./routes/live-stacks"; - import type { config } from "~/typings/database"; +import { liveStacks } from "./routes/live-stacks"; console.log(""); @@ -36,6 +33,14 @@ logger.info("Starting DockStatAPI"); const DockStatAPI = new Elysia() .use(staticPlugin()) .use(serverTiming()) + .use( + dts("./src/index.ts", { + tsconfig: "./tsconfig.json", + compilerOptions: { + strict: true, + }, + }) + ) .use( swagger({ documentation: { From 675c3a104ec3746256840580844eeb9787a55537 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 6 May 2025 18:15:07 +0000 Subject: [PATCH 296/369] Update dependency graphs --- dependency-graph.mmd | 390 ++++++++++++++++++++-------------------- dependency-graph.svg | 411 ++++++++++++++++++++++--------------------- 2 files changed, 409 insertions(+), 392 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 1920bf8b..e54cc0b3 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -8,223 +8,225 @@ flowchart LR subgraph 0["src"] 1["index.ts"] -subgraph 5["routes"] -6["live-stacks.ts"] -U["live-logs.ts"] -1G["api-config.ts"] -1I["docker-manager.ts"] -1J["docker-stats.ts"] -1K["docker-websocket.ts"] -1M["logs.ts"] -1N["stacks.ts"] -1Q["utils.ts"] +subgraph 6["routes"] +7["live-stacks.ts"] +V["live-logs.ts"] +1H["api-config.ts"] +1J["docker-manager.ts"] +1K["docker-stats.ts"] +1L["docker-websocket.ts"] +1N["logs.ts"] +1O["stacks.ts"] +1R["utils.ts"] end -subgraph 8["core"] -subgraph 9["utils"] -A["logger.ts"] -T["helpers.ts"] -14["calculations.ts"] -18["change-me-checker.ts"] -1A["package-json.ts"] -1C["swagger-readme.ts"] -1H["response-handler.ts"] +subgraph 9["core"] +subgraph A["utils"] +B["logger.ts"] +U["helpers.ts"] +15["calculations.ts"] +19["change-me-checker.ts"] +1B["package-json.ts"] +1D["swagger-readme.ts"] +1I["response-handler.ts"] end -subgraph C["database"] -D["_dbState.ts"] -E["index.ts"] -F["backup.ts"] -I["database.ts"] -K["helper.ts"] -L["config.ts"] -M["containerStats.ts"] -N["dockerHosts.ts"] -P["hostStats.ts"] -Q["logs.ts"] -R["stacks.ts"] +subgraph D["database"] +E["_dbState.ts"] +F["index.ts"] +G["backup.ts"] +J["database.ts"] +L["helper.ts"] +M["config.ts"] +N["containerStats.ts"] +O["dockerHosts.ts"] +Q["hostStats.ts"] +R["logs.ts"] +S["stacks.ts"] end -subgraph V["docker"] -W["monitor.ts"] -11["client.ts"] -12["scheduler.ts"] -13["store-container-stats.ts"] -15["store-host-stats.ts"] +subgraph W["docker"] +X["monitor.ts"] +12["client.ts"] +13["scheduler.ts"] +14["store-container-stats.ts"] +16["store-host-stats.ts"] end -subgraph X["plugins"] -Y["plugin-manager.ts"] -17["loader.ts"] +subgraph Y["plugins"] +Z["plugin-manager.ts"] +18["loader.ts"] end -subgraph 1O["stacks"] -1P["controller.ts"] +subgraph 1P["stacks"] +1Q["controller.ts"] end end -subgraph 1D["middleware"] -1E["auth.ts"] +subgraph 1E["middleware"] +1F["auth.ts"] end end subgraph 2["~"] subgraph 3["typings"] 4["database"] -7["websocket"] -G["misc"] -O["docker"] -S["docker-compose"] -Z["plugin"] -16["dockerode"] -1F["elysiajs"] +8["websocket"] +H["misc"] +P["docker"] +T["docker-compose"] +10["plugin"] +17["dockerode"] +1G["elysiajs"] end end -B["path"] -subgraph H["fs"] -19["promises"] +5["elysia-remote-dts"] +C["path"] +subgraph I["fs"] +1A["promises"] end -J["bun:sqlite"] -10["events"] -1B["package.json"] -1L["stream"] -1-->6 -1-->E -1-->W -1-->12 -1-->17 -1-->A -1-->1A -1-->1C -1-->1E -1-->1G -1-->1I +K["bun:sqlite"] +11["events"] +1C["package.json"] +1M["stream"] +1-->7 +1-->F +1-->X +1-->13 +1-->18 +1-->B +1-->1B +1-->1D +1-->1F +1-->1H 1-->1J 1-->1K -1-->U -1-->1M +1-->1L +1-->V 1-->1N -1-->1Q +1-->1O +1-->1R 1-->4 -6-->A -6-->7 -A-->D -A-->E -A-->U -A-->4 -A-->B -E-->F -E-->L -E-->M -E-->I -E-->N -E-->P -E-->Q -E-->R -F-->D -F-->I -F-->K -F-->A +1-->5 +7-->B +7-->8 +B-->E +B-->F +B-->V +B-->4 +B-->C F-->G -F-->H -I-->J -I-->H -K-->D -K-->A -L-->I -L-->K -M-->I -M-->K -N-->I -N-->K -N-->O -P-->I -P-->K -P-->O -Q-->I -Q-->K -Q-->4 -R-->T -R-->I -R-->K +F-->M +F-->N +F-->J +F-->O +F-->Q +F-->R +F-->S +G-->E +G-->J +G-->L +G-->B +G-->H +G-->I +J-->K +J-->I +L-->E +L-->B +M-->J +M-->L +N-->J +N-->L +O-->J +O-->L +O-->P +Q-->J +Q-->L +Q-->P +R-->J +R-->L R-->4 -R-->S -T-->A -U-->A -U-->4 -W-->Y -W-->E -W-->11 -W-->A -W-->O -Y-->A -Y-->O -Y-->Z -Y-->10 -11-->A -11-->O -12-->E -12-->13 -12-->15 -12-->A -12-->4 -13-->A -13-->E -13-->11 +S-->U +S-->J +S-->L +S-->4 +S-->T +U-->B +V-->B +V-->4 +X-->Z +X-->F +X-->12 +X-->B +X-->P +Z-->B +Z-->P +Z-->10 +Z-->11 +12-->B +12-->P +13-->F 13-->14 -15-->E -15-->11 -15-->T -15-->A -15-->O -15-->16 -17-->18 -17-->A -17-->Y -17-->H -17-->B -18-->A +13-->16 +13-->B +13-->4 +14-->B +14-->F +14-->12 +14-->15 +16-->F +16-->12 +16-->U +16-->B +16-->P +16-->17 18-->19 -1A-->1B -1E-->E -1E-->A -1E-->4 -1E-->1F -1G-->E -1G-->F -1G-->Y -1G-->A -1G-->1A -1G-->1H -1G-->1E -1G-->4 -1G-->H -1H-->A +18-->B +18-->Z +18-->I +18-->C +19-->B +19-->1A +1B-->1C +1F-->F +1F-->B +1F-->4 +1F-->1G +1H-->F +1H-->G +1H-->Z +1H-->B +1H-->1B +1H-->1I 1H-->1F -1I-->E -1I-->A -1I-->1H -1I-->O -1J-->E -1J-->11 -1J-->14 -1J-->T -1J-->A -1J-->1H -1J-->O -1J-->16 -1K-->E -1K-->11 -1K-->14 -1K-->A -1K-->1H -1K-->1L -1M-->E -1M-->A -1N-->E -1N-->1P -1N-->A -1N-->1H -1P-->T -1P-->E -1P-->A -1P-->6 -1P-->4 -1P-->S -1P-->19 +1H-->4 +1H-->I +1I-->B +1I-->1G +1J-->F +1J-->B +1J-->1I +1J-->P +1K-->F +1K-->12 +1K-->15 +1K-->U +1K-->B +1K-->1I +1K-->P +1K-->17 +1L-->F +1L-->12 +1L-->15 +1L-->B +1L-->1I +1L-->1M +1N-->F +1N-->B +1O-->F +1O-->1Q +1O-->B +1O-->1I +1Q-->U +1Q-->F +1Q-->B +1Q-->7 +1Q-->4 +1Q-->T 1Q-->1A -1Q-->1H +1R-->1B +1R-->1I diff --git a/dependency-graph.svg b/dependency-graph.svg index 092821e9..6e3dd9ef 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -78,64 +78,73 @@ - + +elysia-remote-dts + + +elysia-remote-dts + + + + + events - + events - + fs - + fs - + fs/promises - + promises - + package.json - + package.json - + path - + path - + src/core/database/_dbState.ts - + _dbState.ts - + src/core/database/backup.ts - + backup.ts @@ -154,9 +163,9 @@ - + src/core/database/database.ts - + database.ts @@ -169,9 +178,9 @@ - + src/core/database/helper.ts - + helper.ts @@ -186,9 +195,9 @@ - + src/core/utils/logger.ts - + logger.ts @@ -203,9 +212,9 @@ - + ~/typings/misc - + misc @@ -256,9 +265,9 @@ - + src/core/database/index.ts - + index.ts @@ -273,9 +282,9 @@ - + ~/typings/database - + database @@ -288,9 +297,9 @@ - + src/routes/live-logs.ts - + live-logs.ts @@ -305,9 +314,9 @@ - + src/core/database/config.ts - + config.ts @@ -328,9 +337,9 @@ - + src/core/database/containerStats.ts - + containerStats.ts @@ -351,9 +360,9 @@ - + src/core/database/dockerHosts.ts - + dockerHosts.ts @@ -374,9 +383,9 @@ - + ~/typings/docker - + docker @@ -389,9 +398,9 @@ - + src/core/database/hostStats.ts - + hostStats.ts @@ -464,9 +473,9 @@ - + src/core/database/logs.ts - + logs.ts @@ -481,9 +490,9 @@ - + src/core/database/stacks.ts - + stacks.ts @@ -538,9 +547,9 @@ - + src/core/utils/helpers.ts - + helpers.ts @@ -555,9 +564,9 @@ - + ~/typings/docker-compose - + docker-compose @@ -578,9 +587,9 @@ - + src/core/docker/client.ts - + client.ts @@ -599,9 +608,9 @@ - + src/core/docker/monitor.ts - + monitor.ts @@ -628,13 +637,13 @@ src/core/docker/monitor.ts->src/core/docker/client.ts - - + + - + src/core/plugins/plugin-manager.ts - + plugin-manager.ts @@ -665,9 +674,9 @@ - + ~/typings/plugin - + plugin @@ -680,9 +689,9 @@ - + src/core/docker/scheduler.ts - + scheduler.ts @@ -707,9 +716,9 @@ - + src/core/docker/store-container-stats.ts - + store-container-stats.ts @@ -722,9 +731,9 @@ - + src/core/docker/store-host-stats.ts - + store-host-stats.ts @@ -751,13 +760,13 @@ src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - + calculations.ts @@ -800,9 +809,9 @@ - + ~/typings/dockerode - + dockerode @@ -815,9 +824,9 @@ - + src/core/plugins/loader.ts - + loader.ts @@ -826,19 +835,19 @@ src/core/plugins/loader.ts->fs - + src/core/plugins/loader.ts->path - + src/core/plugins/loader.ts->src/core/utils/logger.ts - + @@ -848,9 +857,9 @@ - + src/core/utils/change-me-checker.ts - + change-me-checker.ts @@ -875,9 +884,9 @@ - + src/core/stacks/controller.ts - + controller.ts @@ -920,9 +929,9 @@ - + src/routes/live-stacks.ts - + live-stacks.ts @@ -931,32 +940,32 @@ src/core/stacks/controller.ts->src/routes/live-stacks.ts - - + + - + src/routes/live-stacks.ts->src/core/utils/logger.ts - + ~/typings/websocket - + websocket - + src/routes/live-stacks.ts->~/typings/websocket - + src/routes/live-logs.ts->src/core/utils/logger.ts @@ -964,15 +973,15 @@ - + src/routes/live-logs.ts->~/typings/database - - + + - + src/core/utils/package-json.ts - + package-json.ts @@ -985,9 +994,9 @@ - + src/core/utils/response-handler.ts - + response-handler.ts @@ -1000,9 +1009,9 @@ - + ~/typings/elysiajs - + elysiajs @@ -1015,87 +1024,93 @@ - + src/core/utils/swagger-readme.ts - + swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts + + +src/index.ts->elysia-remote-dts + + + src/index.ts->src/core/utils/logger.ts - + src/index.ts->src/core/database/index.ts - + src/index.ts->~/typings/database - - + + src/index.ts->src/core/docker/monitor.ts - - + + src/index.ts->src/core/docker/scheduler.ts - - + + src/index.ts->src/core/plugins/loader.ts - - + + src/index.ts->src/routes/live-stacks.ts - - + + src/index.ts->src/routes/live-logs.ts - - + + src/index.ts->src/core/utils/package-json.ts - - + + src/index.ts->src/core/utils/swagger-readme.ts - + - + src/middleware/auth.ts - + auth.ts @@ -1104,13 +1119,13 @@ src/index.ts->src/middleware/auth.ts - - + + - + src/routes/api-config.ts - + api-config.ts @@ -1119,13 +1134,13 @@ src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - + docker-manager.ts @@ -1134,13 +1149,13 @@ src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - + docker-stats.ts @@ -1149,13 +1164,13 @@ src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - + docker-websocket.ts @@ -1164,13 +1179,13 @@ src/index.ts->src/routes/docker-websocket.ts - + - + src/routes/logs.ts - + logs.ts @@ -1179,13 +1194,13 @@ src/index.ts->src/routes/logs.ts - - + + - + src/routes/stacks.ts - + stacks.ts @@ -1194,13 +1209,13 @@ src/index.ts->src/routes/stacks.ts - - + + - + src/routes/utils.ts - + utils.ts @@ -1209,248 +1224,248 @@ src/index.ts->src/routes/utils.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - + src/middleware/auth.ts->src/core/database/index.ts - + src/middleware/auth.ts->~/typings/database - + src/middleware/auth.ts->~/typings/elysiajs - + src/routes/api-config.ts->fs - + src/routes/api-config.ts->src/core/database/backup.ts - + src/routes/api-config.ts->src/core/utils/logger.ts - + src/routes/api-config.ts->src/core/database/index.ts - + src/routes/api-config.ts->~/typings/database - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - + src/routes/api-config.ts->src/core/utils/package-json.ts - + src/routes/api-config.ts->src/core/utils/response-handler.ts - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - + src/routes/docker-manager.ts->~/typings/docker - + src/routes/docker-manager.ts->src/core/database/index.ts - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - + src/routes/docker-stats.ts->src/core/utils/logger.ts - + src/routes/docker-stats.ts->~/typings/docker - + src/routes/docker-stats.ts->src/core/database/index.ts - + src/routes/docker-stats.ts->src/core/utils/helpers.ts - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - + src/routes/docker-stats.ts->~/typings/dockerode - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - + src/routes/docker-websocket.ts->src/core/database/index.ts - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - + stream - + stream - + src/routes/docker-websocket.ts->stream - + src/routes/logs.ts->src/core/utils/logger.ts - + src/routes/logs.ts->src/core/database/index.ts - + src/routes/stacks.ts->src/core/utils/logger.ts - + src/routes/stacks.ts->src/core/database/index.ts - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - + src/routes/utils.ts->src/core/utils/package-json.ts - + src/routes/utils.ts->src/core/utils/response-handler.ts From ab4968424bb79df4b83235b241b852d6b111d392 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Tue, 6 May 2025 20:20:14 +0200 Subject: [PATCH 297/369] CI/CD: Always publish test report --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1c83b24..3047c175 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,7 @@ jobs: bun clean - name: Publish Test Report + if: always() uses: mikepenz/action-junit-report@v5 with: report_paths: "unit-test.xml" From 64a9b9c61b87a00f2a0d7e789394bc5a9e51057e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 8 May 2025 22:23:52 +0200 Subject: [PATCH 298/369] Feat: Bunch of changes, see future note --- biome.json | 51 +- docker/docker-compose.dev.yaml | 2 +- package.json | 19 +- src/core/database/logs.ts | 6 +- src/core/utils/calculations.ts | 2 +- src/index.ts | 251 +++---- src/routes/api-config.ts | 1143 ++++++++++++++++---------------- src/routes/docker-manager.ts | 3 +- src/routes/docker-stats.ts | 1028 +++++++++++++++------------- src/routes/logs.ts | 10 +- src/tests/api-config.spec.ts | 270 ++++++++ src/tests/cleanup.ts | 21 - src/tests/delete.spec.ts | 13 - src/tests/docker-manager.ts | 327 +++++++++ src/tests/gets.spec.ts | 61 -- src/tests/helper.ts | 129 ---- src/tests/junit-exporter.ts | 79 +++ src/tests/post.spec.ts | 52 -- 18 files changed, 1989 insertions(+), 1478 deletions(-) create mode 100644 src/tests/api-config.spec.ts delete mode 100644 src/tests/cleanup.ts delete mode 100644 src/tests/delete.spec.ts create mode 100644 src/tests/docker-manager.ts delete mode 100644 src/tests/gets.spec.ts delete mode 100644 src/tests/helper.ts create mode 100644 src/tests/junit-exporter.ts delete mode 100644 src/tests/post.spec.ts diff --git a/biome.json b/biome.json index bdb5eccf..f9d82247 100644 --- a/biome.json +++ b/biome.json @@ -1,26 +1,29 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "formatter": { - "enabled": true, - "indentStyle": "tab" - }, - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double" - } - } + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "files": { + "ignore": ["./src/tests/junit-exporter.ts"] + } } diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index 878d1f0a..799ae7a8 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -47,6 +47,6 @@ services: ports: - 8080:8080 volumes: - - ../:/data:ro + - ../data:/data:ro environment: - SQLITE_DATABASE=dockstatapi.db diff --git a/package.json b/package.json index 3217c54d..8fb4fe58 100644 --- a/package.json +++ b/package.json @@ -18,17 +18,18 @@ "build:prod": "NODE_ENV=production bun build --no-native --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts", "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", "clean": "bun run clean:win || bun run clean:lin", - "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi*.db* && echo 'success'", - "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi*.db* && echo 'success'", + "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi* && cmd /c del /Q reports/junit/*.xml && echo 'success'", + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi* && rm -f reports/junit/*.xml && echo 'success'", "knip": "knip", "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src", "test": "bun test src/tests/**/*.test.ts" }, "dependencies": { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - "@elysiajs/swagger": "^1.2.2", + "@elysiajs/server-timing": "^1.3.0", + "@elysiajs/static": "^1.3.0", + "@elysiajs/swagger": "^1.3.0", "chalk": "^5.4.1", + "date-fns": "^4.1.0", "docker-compose": "^1.2.0", "dockerode": "^4.0.6", "elysia": "latest", @@ -40,15 +41,15 @@ }, "devDependencies": { "@biomejs/biome": "1.9.4", + "@types/bun": "latest", "@types/dockerode": "^3.3.38", - "@types/node": "^22.15.3", + "@types/node": "^22.15.17", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", "logform": "^2.7.0", "typescript": "^5.8.3", - "wrap-ansi": "^9.0.0", - "@types/bun": "latest" + "wrap-ansi": "^9.0.0" }, "module": "src/index.js", "trustedDependencies": [ @@ -56,4 +57,4 @@ ], "type": "module", "private": true -} \ No newline at end of file +} diff --git a/src/core/database/logs.ts b/src/core/database/logs.ts index f6b99802..eb815d54 100644 --- a/src/core/database/logs.ts +++ b/src/core/database/logs.ts @@ -4,13 +4,13 @@ import { executeDbOperation } from "./helper"; const stmt = { insert: db.prepare( - "INSERT INTO backend_log_entries (timestamp, level, message, file, line) VALUES (?, ?, ?, ?, ?)", + "INSERT INTO backend_log_entries (level, timestamp, message, file, line) VALUES (?, ?, ?, ?, ?)", ), selectAll: db.prepare( - "SELECT timestamp, level, message, file, line FROM backend_log_entries ORDER BY timestamp DESC", + "SELECT level, timestamp, message, file, line FROM backend_log_entries ORDER BY timestamp DESC", ), selectByLevel: db.prepare( - "SELECT timestamp, level, message, file, line FROM backend_log_entries WHERE level = ?", + "SELECT level, timestamp, message, file, line FROM backend_log_entries WHERE level = ?", ), deleteAll: db.prepare("DELETE FROM backend_log_entries"), deleteByLevel: db.prepare("DELETE FROM backend_log_entries WHERE level = ?"), diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index b640c47a..fbb7a422 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -31,7 +31,7 @@ const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; - return data ; + return data; }; export { calculateCpuPercent, calculateMemoryUsage }; diff --git a/src/index.ts b/src/index.ts index e2d478ea..419c6bf3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; import { swagger } from "@elysiajs/swagger"; -import { Elysia, t } from "elysia"; +import { Elysia } from "elysia"; import { dts } from "elysia-remote-dts"; import { dbFunctions } from "~/core/database"; import { monitorDockerEvents } from "~/core/docker/monitor"; @@ -9,9 +9,9 @@ import { setSchedules } from "~/core/docker/scheduler"; import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { - authorWebsite, - contributors, - license, + authorWebsite, + contributors, + license, } from "~/core/utils/package-json"; import { swaggerReadme } from "~/core/utils/swagger-readme"; import { validateApiKey } from "~/middleware/auth"; @@ -31,137 +31,140 @@ console.log(""); logger.info("Starting DockStatAPI"); const DockStatAPI = new Elysia() - .use(staticPlugin()) - .use(serverTiming()) - .use( - dts("./src/index.ts", { - tsconfig: "./tsconfig.json", - compilerOptions: { - strict: true, - }, - }) - ) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey" as const, - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }) - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; + .use(staticPlugin()) + .use(serverTiming()) + .use( + dts("./src/index.ts", { + tsconfig: "./tsconfig.json", + compilerOptions: { + strict: true, + }, + }), + ) + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "3.0.0", + description: swaggerReadme, + }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey" as const, + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], + }, + }), + ) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; - if ( - path === "/health" || - path.startsWith("/swagger") || - path.startsWith("/trpc") - ) { - logger.info(`Requested unguarded route: ${path}`); - return; - } + if ( + path === "/health" || + path.startsWith("/swagger") || + path.startsWith("/trpc") + ) { + logger.info(`Requested unguarded route: ${path}`); + return; + } - const validation = await validateApiKey(request, set); + const validation = await validateApiKey(request, set); - if (validation.error) { - set.status = 400; - set.headers["Content-Type"] = "application/json"; - return { error: validation.error }; - } - }) - .onError(({ code, set, path }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(utilRoutes) - .use(stackRoutes) - .use(liveLogs) - .use(liveStacks) - .get("/health", () => ({ status: "healthy" }), { tags: ["Utils"] }) - .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger` - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }); + if (validation.error) { + set.status = 400; + + return { error: validation.error }; + } + }) + .onError(({ code, set, path }) => { + if (code === "NOT_FOUND") { + logger.warn(`Unknown route (${path}), showing error page!`); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); + } + }) + .use(dockerRoutes) + .use(dockerStatsRoutes) + .use(backendLogs) + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .use(utilRoutes) + .use(stackRoutes) + .use(liveLogs) + .use(liveStacks) + .get("/health", () => ({ status: "healthy" }), { + tags: ["Utils"], + response: { message: "healthy" }, + }) + .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + }); const initializeServer = async () => { - try { - await loadPlugins("./src/plugins"); - await setSchedules(); + try { + await loadPlugins("./src/plugins"); + await setSchedules(); - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" - ); - } + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", + ); + } - logger.info("Started server"); - console.log("----- [ ############## ]"); - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } + logger.info("Started server"); + console.log("----- [ ############## ]"); + } catch (error) { + logger.error("Error while starting server:", error); + process.exit(1); + } }; await initializeServer(); -export { DockStatAPI }; export type App = typeof DockStatAPI; +export { DockStatAPI }; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 79749e6f..7e51d8b3 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -4,15 +4,15 @@ import { dbFunctions } from "~/core/database"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import { responseHandler } from "~/core/utils/response-handler"; @@ -21,577 +21,576 @@ import { hashApiKey } from "~/middleware/auth"; import type { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "/", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; - set.headers["Content-Type"] = "application/json"; - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting the DockStatAPI config" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - responses: { - "200": { - description: "Successfully retrieved configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - fetching_interval: { - type: "number", - example: 5, - }, - keep_data_for: { - type: "number", - example: 7, - }, - api_key: { - type: "string", - example: "hashed_api_key", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/plugins", - ({ set }) => { - try { - return pluginManager.getLoadedPlugins(); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting all registered plugins" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - responses: { - "200": { - description: "Successfully retrieved plugins", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-plugin", - }, - version: { - type: "string", - example: "1.0.0", - }, - status: { - type: "string", - example: "active", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving plugins", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting all registered plugins", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; - set.headers["Content-Type"] = "application/json"; - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key) - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - return responseHandler.error( - set, - "Error updating the DockStatAPI config", - error as string - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - responses: { - "200": { - description: "Successfully updated configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated DockStatAPI config", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error updating the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - } - ) - .get( - "/package", - async ({ set }) => { - try { - logger.debug("Fetching package.json"); - return { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error while reading package.json" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - responses: { - "200": { - description: "Successfully retrieved package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error while reading package.json", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .post( - "/backup", - async ({ set }) => { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return responseHandler.ok(set, backupFilename); - } catch (error) { - return responseHandler.error(set, error as string, "Error backing up"); - } - }, - { - detail: { - tags: ["Management"], - description: "Backs up the internal database", - responses: { - "200": { - description: "Successfully created backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "backup_2024-03-20_12-00-00.db.bak", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error creating backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error backing up", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/backup", - async ({ set }) => { - try { - const backupFiles = readdirSync(backupDir); + .get( + "", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error getting the DockStatAPI config", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Returns current API configuration including data retention policies and security settings", + responses: { + "200": { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fetching_interval: { + type: "number", + example: 5, + }, + keep_data_for: { + type: "number", + example: 7, + }, + api_key: { + type: "string", + example: "hashed_api_key", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/plugins", + ({ set }) => { + try { + return pluginManager.getLoadedPlugins(); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error getting all registered plugins", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + responses: { + "200": { + description: "Successfully retrieved plugins", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-plugin", + }, + version: { + type: "string", + example: "1.0.0", + }, + status: { + type: "string", + example: "active", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving plugins", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting all registered plugins", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { fetching_interval, keep_data_for, api_key } = body; - return filteredFiles; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Reading Backup directory" - ); - } - }, - { - detail: { - tags: ["Management"], - description: "Lists all available backups", - responses: { - "200": { - description: "Successfully retrieved backup list", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "string", - }, - example: [ - "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak", - ], - }, - }, - }, - }, - "400": { - description: "Error retrieving backup list", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Reading Backup directory", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key), + ); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + return responseHandler.error( + set, + "Error updating the DockStatAPI config", + error as string, + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", + responses: { + "200": { + description: "Successfully updated configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated DockStatAPI config", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error updating the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), + }, + ) + .get( + "/package", + async ({ set }) => { + try { + logger.debug("Fetching package.json"); + return { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Error while reading package.json", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Displays package metadata including dependencies, contributors, and licensing information", + responses: { + "200": { + description: "Successfully retrieved package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0", + }, + description: { + type: "string", + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", + }, + license: { + type: "string", + example: "CC BY-NC 4.0", + }, + authorName: { + type: "string", + example: "ItsNik", + }, + authorEmail: { + type: "string", + example: "info@itsnik.de", + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik", + }, + contributors: { + type: "array", + items: { + type: "string", + }, + example: [], + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + }, + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error while reading package.json", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .post( + "/backup", + async ({ set }) => { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return responseHandler.ok(set, backupFilename); + } catch (error) { + return responseHandler.error(set, error as string, "Error backing up"); + } + }, + { + detail: { + tags: ["Management"], + description: "Backs up the internal database", + responses: { + "200": { + description: "Successfully created backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "backup_2024-03-20_12-00-00.db.bak", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error creating backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error backing up", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/backup", + async ({ set }) => { + try { + const backupFiles = readdirSync(backupDir); - .get( - "/backup/download", - async ({ query, set }) => { - try { - const filename = query.filename || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } + return filteredFiles; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Reading Backup directory", + ); + } + }, + { + detail: { + tags: ["Management"], + description: "Lists all available backups", + responses: { + "200": { + description: "Successfully retrieved backup list", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "string", + }, + example: [ + "backup_2024-03-20_12-00-00.db.bak", + "backup_2024-03-19_12-00-00.db.bak", + ], + }, + }, + }, + }, + "400": { + description: "Error retrieving backup list", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Reading Backup directory", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) - set.headers["Content-Type"] = "application/octet-stream"; - set.headers[ - "Content-Disposition" - ] = `attachment; filename="${filename}"`; - return Bun.file(filePath); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Backup download failed" - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Download a specific backup or the latest if no filename is provided", - responses: { - "200": { - description: "Successfully downloaded backup file", - content: { - "application/octet-stream": { - schema: { - type: "string", - format: "binary", - example: "Binary backup file content", - }, - }, - }, - headers: { - "Content-Disposition": { - schema: { - type: "string", - example: - 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', - }, - }, - }, - }, - "400": { - description: "Error downloading backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Backup download failed", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - filename: t.Optional(t.String()), - }), - } - ) - .post( - "/restore", - async ({ body, set }) => { - try { - const { file } = body; + .get( + "/backup/download", + async ({ query, set }) => { + try { + const filename = query.filename || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; - set.headers["Content-Type"] = "text/html"; + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } - if (!file) { - throw new Error("No file uploaded"); - } + set.headers["Content-Type"] = "application/octet-stream"; + set.headers["Content-Disposition"] = + `attachment; filename="${filename}"`; + return Bun.file(filePath); + } catch (error) { + return responseHandler.error( + set, + error as string, + "Backup download failed", + ); + } + }, + { + detail: { + tags: ["Management"], + description: + "Download a specific backup or the latest if no filename is provided", + responses: { + "200": { + description: "Successfully downloaded backup file", + content: { + "application/octet-stream": { + schema: { + type: "string", + format: "binary", + example: "Binary backup file content", + }, + }, + }, + headers: { + "Content-Disposition": { + schema: { + type: "string", + example: + 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', + }, + }, + }, + }, + "400": { + description: "Error downloading backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Backup download failed", + }, + }, + }, + }, + }, + }, + }, + }, + query: t.Object({ + filename: t.Optional(t.String()), + }), + }, + ) + .post( + "/restore", + async ({ body, set }) => { + try { + const { file } = body; - if (!file.name.endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } + set.headers["Content-Type"] = "text/html"; - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); + if (!file) { + throw new Error("No file uploaded"); + } - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); + if (!file.name.endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } - return responseHandler.ok(set, "Database restored successfully"); - } catch (error) { - return responseHandler.error( - set, - error instanceof Error ? error.message : "Restoration failed", - "Database restoration error" - ); - } - }, - { - body: t.Object({ file: t.File() }), - detail: { - tags: ["Management"], - description: "Restore database from uploaded backup file", - responses: { - "200": { - description: "Successfully restored database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Database restored successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restoring database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Database restoration error", - }, - }, - }, - }, - }, - }, - }, - }, - } - ); + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return responseHandler.ok(set, "Database restored successfully"); + } catch (error) { + return responseHandler.error( + set, + error instanceof Error ? error.message : "Restoration failed", + "Database restoration error", + ); + } + }, + { + body: t.Object({ file: t.File() }), + detail: { + tags: ["Management"], + description: "Restore database from uploaded backup file", + responses: { + "200": { + description: "Successfully restored database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Database restored successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error restoring database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Database restoration error", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 68044cdf..30cd5c46 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -11,7 +11,6 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) "/add-host", async ({ set, body }) => { try { - set.headers["Content-Type"] = "application/json"; dbFunctions.addDockerHost(body as DockerHost); return responseHandler.ok(set, `Added docker host (${body.name})`); } catch (error: unknown) { @@ -139,7 +138,7 @@ export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) async ({ set }) => { try { const dockerHosts = dbFunctions.getDockerHosts(); - set.headers["Content-Type"] = "application/json"; + logger.debug("Retrieved docker hosts"); return dockerHosts; } catch (error) { diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index 42b887d3..b411f2d4 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -4,8 +4,8 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; @@ -15,481 +15,587 @@ import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get( - "/containers", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; + .get( + "/containers", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - return responseHandler.error( - set, - pingError as string, - "Docker host connection failed" - ); - } + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + return responseHandler.error( + set, + pingError as string, + "Docker host connection failed", + ); + } - const hostContainers = await docker.listContainers({ all: true }); + const hostContainers = await docker.listContainers({ all: true }); - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - return responseHandler.reject( - set, - reject, - "An error occurred", - error - ); - } - if (!stats) { - return responseHandler.reject( - set, - reject, - "No stats available" - ); - } - resolve(stats); - }); - } - ); + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return responseHandler.reject( + set, + reject, + "An error occurred", + error, + ); + } + if (!stats) { + return responseHandler.reject( + set, + reject, + "No stats available", + ); + } + resolve(stats); + }); + }, + ); - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError - ); - } - }) - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (hostError) { - logger.error("Error fetching containers for host,", hostError); - } - }) - ); + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (hostError) { + logger.error("Error fetching containers for host,", hostError); + } + }), + ); - set.headers["Content-Type"] = "application/json"; - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve containers" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", - responses: { - "200": { - description: "Successfully retrieved container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - containers: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "string", - example: "abc123def456", - }, - hostId: { - type: "string", - example: "1", - }, - name: { - type: "string", - example: "example-container", - }, - image: { - type: "string", - example: "nginx:latest", - }, - status: { - type: "string", - example: "running", - }, - state: { - type: "string", - example: "running", - }, - cpuUsage: { - type: "number", - example: 0.5, - }, - memoryUsage: { - type: "number", - example: 1024, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve containers", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve containers", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", + responses: { + "200": { + description: "Successfully retrieved container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + containers: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + example: "abc123def456", + }, + hostId: { + type: "string", + example: "1", + }, + name: { + type: "string", + example: "example-container", + }, + image: { + type: "string", + example: "nginx:latest", + }, + status: { + type: "string", + example: "running", + }, + state: { + type: "string", + example: "running", + }, + cpuUsage: { + type: "number", + example: 0.5, + }, + memoryUsage: { + type: "number", + example: 1024, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve containers", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - set.headers["Content-Type"] = "application/json"; - logger.debug("Fetched all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/hosts/:id", - async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - if (!params.id) { - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - return stats; - } + logger.debug("Fetched stats for all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for all hosts", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts/:id", + async ({ params, set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = findObjectByKey(hosts, "id", Number(params.id)); - if (!host) { - return responseHandler.simple_error( - set, - `Host (${params.id}) not found` - ); - } + const host = findObjectByKey(hosts, "id", Number(params.id)); + if (!host) { + return responseHandler.simple_error( + set, + `Host (${params.id}) not found`, + ); + } - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - set.headers["Content-Type"] = "application/json"; - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ); + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ); diff --git a/src/routes/logs.ts b/src/routes/logs.ts index a4feb152..17da1fb7 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -5,11 +5,11 @@ import { logger } from "~/core/utils/logger"; export const backendLogs = new Elysia({ prefix: "/logs" }) .get( - "/", + "", async ({ set }) => { try { const logs = dbFunctions.getAllLogs(); - set.headers["Content-Type"] = "application/json"; + // logger.debug("Retrieved all logs"); return logs; } catch (error) { @@ -81,7 +81,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) async ({ params: { level }, set }) => { try { const logs = dbFunctions.getLogsByLevel(level); - set.headers["Content-Type"] = "application/json"; + logger.debug(`Retrieved logs (level: ${level})`); return logs; } catch (error) { @@ -153,7 +153,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) async ({ set }) => { try { set.status = 200; - set.headers["Content-Type"] = "application/json"; + dbFunctions.clearAllLogs(); return { success: true }; } catch (error) { @@ -209,7 +209,7 @@ export const backendLogs = new Elysia({ prefix: "/logs" }) async ({ params: { level }, set }) => { try { dbFunctions.clearLogsByLevel(level); - set.headers["Content-Type"] = "application/json"; + logger.debug(`Cleared all logs with level: ${level}`); return { success: true }; } catch (error) { diff --git a/src/tests/api-config.spec.ts b/src/tests/api-config.spec.ts new file mode 100644 index 00000000..1241885a --- /dev/null +++ b/src/tests/api-config.spec.ts @@ -0,0 +1,270 @@ +import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; +import { Elysia } from "elysia"; +import { logger } from "~/core/utils/logger"; +import { apiConfigRoutes } from "~/routes/api-config"; +import { + generateJunitReport, + recordTestResult, + testResults, +} from "./junit-exporter"; + +const mockDb = { + getConfig: mock(() => [ + { + fetching_interval: 10, + keep_data_for: 14, + api_key: "$argon2id$v=19$m=65536,t=2,p=1$...", + }, + ]), + updateConfig: mock(), + backupDatabase: mock( + () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak`, + ), + restoreDatabase: mock(), + findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), +}; + +mock.module("node:fs", () => ({ + existsSync: mock((path) => path.includes("dockstatapi")), + readdirSync: mock(() => [ + "dockstatapi-2025-05-06.db.bak", + "dockstatapi.db", + "dockstatapi.db-shm", + ]), + unlinkSync: mock(), +})); + +const mockPlugins = [ + { + name: "docker-monitor", + version: "1.2.0", + status: "active", + }, +]; + +const createTestApp = () => + new Elysia().use(apiConfigRoutes).decorate({ + db: mockDb, + pluginManager: { + getLoadedPlugins: mock(() => mockPlugins), + getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), + }, + logger: { + ...logger, + debug: mock(), + error: mock(), + info: mock(), + }, + }); + +describe("API Configuration Endpoints", () => { + beforeEach(() => { + mockDb.getConfig.mockClear(); + mockDb.updateConfig.mockClear(); + }); + + describe("Core Configuration", () => { + it("should retrieve current config with hashed API key", async () => { + const start = Date.now(); + try { + const app = createTestApp(); + const res = await app.handle( + new Request("http://localhost:3000/config"), + ); + + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toMatchObject({ + fetching_interval: expect.any(Number), + keep_data_for: expect.any(Number), + }); + + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + + it("should handle config update with valid payload", async () => { + const start = Date.now(); + try { + const app = createTestApp(); + const res = await app.handle( + new Request("http://localhost:3000/config/update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + fetching_interval: 15, + keep_data_for: 30, + api_key: "new-valid-key", + }), + }), + ); + + expect(res.status).toBe(200); + expect(await res.json()).toMatchObject({ + success: true, + message: expect.stringContaining("Updated"), + }); + + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); + + describe("Plugin Management", () => { + it("should list active plugins with metadata", async () => { + const start = Date.now(); + try { + const app = createTestApp(); + const res = await app.handle( + new Request("http://localhost:3000/config/plugins"), + ); + + expect(res.status).toBe(200); + expect(await res.json()).toEqual([]); + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); + + describe("Backup Management", () => { + it("should generate timestamped backup files", async () => { + const start = Date.now(); + try { + const app = createTestApp(); + const res = await app.handle( + new Request("http://localhost:3000/config/backup", { + method: "POST", + }), + ); + + expect(res.status).toBe(200); + const { message } = await res.json(); + expect(message).toMatch( + /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/, + ); + + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + + it("should list valid backup files", async () => { + const start = Date.now(); + try { + const app = createTestApp(); + const res = await app.handle( + new Request("http://localhost:3000/config/backup"), + ); + + expect(res.status).toBe(200); + const backups = await res.json(); + expect(backups).toEqual( + expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]), + ); + + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); + + describe("Error Handling", () => { + it("should return proper error format", async () => { + const start = Date.now(); + try { + mockDb.getConfig.mockImplementationOnce(() => { + throw new Error("Database connection failed"); + }); + + const app = createTestApp(); + const res = await app.handle( + new Request("http://localhost:3000/config"), + ); + + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toMatchObject({ + api_key: expect.stringMatching(/^\$argon2id\$/), + fetching_interval: 15, + keep_data_for: 30, + }); + + recordTestResult({ + name: "should return proper error format", + suite: "API Configuration Endpoints - Error Handling", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "should return proper error format", + suite: "API Configuration Endpoints - Error Handling", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); +}); + +afterAll(() => { + generateJunitReport(); +}); diff --git a/src/tests/cleanup.ts b/src/tests/cleanup.ts deleted file mode 100644 index ac90de71..00000000 --- a/src/tests/cleanup.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { dbFunctions } from "~/core/database"; -import { findObjectByKey } from "~/core/utils/helpers"; - -import type { DockerHost } from "~/typings/docker"; - -console.log(""); -console.log("Deleting `test` Docker host"); - -const testHosts: DockerHost[] = dbFunctions.getDockerHosts(); - -const testHost = findObjectByKey(testHosts, "name", "test"); - -if (testHost) { - dbFunctions.deleteDockerHost(testHost.id as number); - console.log(`Docker host with name "${testHost.name}" deleted.`); -} else { - console.log("Docker host not found."); -} - -console.log("Cleaning up Database config to default values"); -dbFunctions.updateConfig(5, 7, "changeme"); diff --git a/src/tests/delete.spec.ts b/src/tests/delete.spec.ts deleted file mode 100644 index 901b05f6..00000000 --- a/src/tests/delete.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { describe, it } from "bun:test"; - -import { runTestCode } from "./helper"; - -describe("DockStatAPI (DELETE)", () => { - it("Delete all Logs /logs", async () => { - await runTestCode("/logs", 200, "DELETE", {}); - }); - - it("Delete Logs (Debug) /logs/debug", async () => { - await runTestCode("/logs/debug", 200, "DELETE", {}); - }); -}); diff --git a/src/tests/docker-manager.ts b/src/tests/docker-manager.ts new file mode 100644 index 00000000..4c95916e --- /dev/null +++ b/src/tests/docker-manager.ts @@ -0,0 +1,327 @@ +import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; +import { Elysia } from "elysia"; +import { dbFunctions } from "~/core/database"; +import { dockerRoutes } from "~/routes/docker-manager"; +import { + generateJunitReport, + recordTestResult, + testResults, +} from "./junit-exporter"; + +type DockerHost = { + id?: number; + name: string; + hostAddress: string; + secure: boolean; +}; + +mock.module("~/core/database", () => ({ + dbFunctions: { + addDockerHost: mock(), + updateDockerHost: mock(), + getDockerHosts: mock(), + deleteDockerHost: mock(), + }, +})); + +// Silence logger +mock.module("~/core/utils/logger", () => ({ + logger: { debug: mock(), info: mock(), error: mock() }, +})); + +const createApp = () => new Elysia().use(dockerRoutes).decorate({}); + +describe("Docker Configuration Endpoints", () => { + beforeEach(() => { + // Clear mocks and testResults + testResults.length = 0; + Object.values(dbFunctions).forEach((fn) => fn.mockClear()); + }); + + describe("POST /docker-config/add-host", () => { + it("should add a docker host successfully", async () => { + const start = Date.now(); + const host: DockerHost = { + name: "Host1", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + try { + const app = createApp(); + const res = await app.handle( + new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }), + ); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toMatchObject({ + message: `Added docker host (${host.name})`, + }); + expect(dbFunctions.addDockerHost).toHaveBeenCalledWith(host); + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + + it("should handle error when adding a docker host fails", async () => { + const start = Date.now(); + const host: DockerHost = { + name: "Host2", + hostAddress: "invalid", + secure: true, + }; + dbFunctions.addDockerHost.mockImplementationOnce(() => { + throw new Error("DB error"); + }); + try { + const app = createApp(); + const res = await app.handle( + new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }), + ); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data).toHaveProperty("error"); + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); + + describe("POST /docker-config/update-host", () => { + it("should update a docker host successfully", async () => { + const start = Date.now(); + const host: DockerHost = { + id: 1, + name: "Host1-upd", + hostAddress: "127.0.0.1:2376", + secure: true, + }; + try { + const app = createApp(); + const res = await app.handle( + new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }), + ); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toMatchObject({ + message: `Updated docker host (${host.id})`, + }); + expect(dbFunctions.updateDockerHost).toHaveBeenCalledWith(host); + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + + it("should handle error when update fails", async () => { + const start = Date.now(); + const host: DockerHost = { + id: 2, + name: "Host2", + hostAddress: "x", + secure: false, + }; + dbFunctions.updateDockerHost.mockImplementationOnce(() => { + throw new Error("Update error"); + }); + try { + const app = createApp(); + const res = await app.handle( + new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }), + ); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data).toHaveProperty("error"); + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); + + describe("GET /docker-config/hosts", () => { + it("should retrieve list of hosts", async () => { + const start = Date.now(); + const hosts: DockerHost[] = [ + { id: 1, name: "H1", hostAddress: "a", secure: false }, + ]; + dbFunctions.getDockerHosts.mockReturnValueOnce(hosts); + try { + const app = createApp(); + const res = await app.handle( + new Request("http://localhost/docker-config/hosts"), + ); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toEqual(hosts); + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + + it("should handle error when retrieval fails", async () => { + const start = Date.now(); + dbFunctions.getDockerHosts.mockImplementationOnce(() => { + throw new Error("Fetch error"); + }); + try { + const app = createApp(); + const res = await app.handle( + new Request("http://localhost/docker-config/hosts"), + ); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data).toHaveProperty("error"); + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); + + describe("DELETE /docker-config/hosts/:id", () => { + it("should delete a host successfully", async () => { + const start = Date.now(); + const id = 5; + try { + const app = createApp(); + const res = await app.handle( + new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }), + ); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toMatchObject({ message: `Deleted docker host (${id})` }); + expect(dbFunctions.deleteDockerHost).toHaveBeenCalledWith(id); + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + + it("should handle error when delete fails", async () => { + const start = Date.now(); + const id = 6; + dbFunctions.deleteDockerHost.mockImplementationOnce(() => { + throw new Error("Delete error"); + }); + try { + const app = createApp(); + const res = await app.handle( + new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }), + ); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data).toHaveProperty("error"); + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + }); + } catch (error) { + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + }); + throw error; + } + }); + }); +}); + +afterAll(() => { + generateJunitReport(); +}); diff --git a/src/tests/gets.spec.ts b/src/tests/gets.spec.ts deleted file mode 100644 index e542a0f4..00000000 --- a/src/tests/gets.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, it } from "bun:test"; - -import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, -} from "~/core/utils/package-json"; - -import { runTestCode, runTestResponse } from "./helper"; - -describe("DockStatAPI (GET)", () => { - it("Check Server connection", async () => { - await runTestResponse("/health", '{"status":"healthy"}', "GET"); - }); - - it("Check /docker/containers", async () => { - await runTestCode("/docker/containers", 200, "GET"); - }); - - it("Check /docker/hosts/Localhost", async () => { - await runTestCode("/docker/hosts/Localhost", 200, "GET"); - }); - - it("Check /docker-config/hosts", async () => { - await runTestCode("/docker-config/hosts", 200, "GET"); - }); - - it("Check /logs/", async () => { - await runTestCode("/logs", 200, "GET"); - }); - - it("Check /logs/debug", async () => { - await runTestCode("/logs/debug", 200, "GET"); - }); - - it("Check /config", async () => { - await runTestCode("/config", 200, "GET"); - }); - - it("Check /config/package", async () => { - const expected = JSON.stringify({ - version, - description, - license, - authorName, - authorEmail, - authorWebsite, - contributors, - dependencies, - devDependencies, - }); - - await runTestResponse("/config/package", expected, "GET"); - }); -}); diff --git a/src/tests/helper.ts b/src/tests/helper.ts deleted file mode 100644 index 60161113..00000000 --- a/src/tests/helper.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { expect } from "bun:test"; - -import { logger } from "~/core/utils/logger"; - -import { DockStatAPI } from ".."; - -export const API_KEY = "TestKey"; - -const host = "http://localhost"; -const port = process.env.DOCKSTATAPI_PORT || 3000; -const server = `${host}:${port}`; - -export async function runTestResponse( - path: string, - expected_response: string, - method: "GET" | "POST" | "DELETE" = "GET", - requestBody?: string, -) { - const route = `${server}${path}`; - - logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); - const startTime = Date.now(); - - try { - const processedBody = - requestBody !== undefined - ? typeof requestBody === "string" - ? requestBody - : JSON.stringify(requestBody) - : undefined; - - const request = new Request(route, { - method, - body: processedBody, - headers: { - "Content-Type": "application/json", - "x-api-key": API_KEY, - }, - }); - - logger.debug( - `Request details: ${JSON.stringify({ - url: route, - method, - headers: [...request.headers], - body: processedBody, - })}`, - ); - - const response = await DockStatAPI.handle(request); - const headers: { [key: string]: string } = {}; - - response.headers.forEach((value, key) => { - headers[key] = value; - }); - - const responseText = await response.text(); - const duration = Date.now() - startTime; - - logger.debug(`Received HTTP status: ${response.status}`); - logger.debug(`Response headers: ${JSON.stringify(headers)}`); - logger.debug(`Response body: ${responseText}`); - logger.debug(`Total Duration: ${duration}ms`); - - expect(responseText).toBe(expected_response); - logger.info(`__UT__ [ END ] Completed test on ${route}`); - } catch (error) { - logger.error(`__UT__ Error during test on ${route}: ${error}`); - throw error; - } -} - -export async function runTestCode( - path: string, - expected_code: number, - method: "GET" | "POST" | "DELETE" = "GET", - requestBody?: object, -) { - const route = `${server}${path}`; - - logger.info(`__UT__ [ START ] Running test, method: ${method} on ${route}`); - const startTime = Date.now(); - - try { - const processedBody = - requestBody !== undefined - ? typeof requestBody === "string" - ? requestBody - : JSON.stringify(requestBody) - : undefined; - - const request = new Request(route, { - method, - body: processedBody, - headers: { - "Content-Type": "application/json", - "x-api-key": API_KEY, - }, - }); - - logger.debug( - `Request details: ${JSON.stringify({ - url: route, - method, - headers: [...request.headers], - body: processedBody, - })}`, - ); - - const response = await DockStatAPI.handle(request); - const headers: { [key: string]: string } = {}; - - response.headers.forEach((value, key) => { - headers[key] = value; - }); - - const duration = Date.now() - startTime; - - logger.debug(`Received HTTP status: ${response.status}`); - logger.debug(`Response headers: ${JSON.stringify(headers)}`); - logger.debug(`Response body: ${JSON.stringify(response.body)}`); - - expect(response.status).toBe(expected_code); - logger.debug(`__UT__ Completed test on ${route} (Duration: ${duration}ms)`); - } catch (error) { - logger.error(`__UT__ Error during test on ${route}: ${error}`); - throw error; - } -} diff --git a/src/tests/junit-exporter.ts b/src/tests/junit-exporter.ts new file mode 100644 index 00000000..ae7b78f6 --- /dev/null +++ b/src/tests/junit-exporter.ts @@ -0,0 +1,79 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { format } from "date-fns"; +import { logger } from "~/core/utils/logger"; + +type TestResult = { + name: string; + suite: string; + time: number; + error?: Error; +}; + +export function recordTestResult(result: TestResult) { + logger.debug(`__UT__ Recording test result: ${JSON.stringify(result)}`); + testResults.push(result); +} + +export let testResults: TestResult[] = []; + +export function generateJunitReport() { + if (testResults.length === 0) { + logger.warn("No test results to generate JUnit report."); + return; + } + + const totalTests = testResults.length; + const totalErrors = testResults.filter((r) => r.error).length; + + const testSuites = testResults.reduce((suites, result) => { + if (!suites[result.suite]) { + suites[result.suite] = []; + } + suites[result.suite].push(result); + return suites; + }, {} as Record); + + const xml = ` + + ${Object.entries(testSuites) + .map(([suiteName, cases]) => { + const suiteErrors = cases.filter((c) => c.error).length; + return ` + + ${cases + .map( + (testCase) => ` + + ${ + testCase.error + ? ` + + ${testCase.error.stack?.replace(//g, "]]]]>>")} + ` + : "" + } + ` + ) + .join("\n")} + `; + }) + .join("\n")} +`; + + mkdirSync("reports/junit", { recursive: true }); + writeFileSync( + `reports/junit/junit-${format(new Date(), "yyyy-MM-dd")}.xml`, + xml, + "utf8" + ); + + // Clear results after reporting + // resetTestResults(); + + logger.debug(`__UT__ Final data: ${JSON.stringify(testResults)}`); +} diff --git a/src/tests/post.spec.ts b/src/tests/post.spec.ts deleted file mode 100644 index a4933dcc..00000000 --- a/src/tests/post.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, it } from "bun:test"; - -import { runTestCode, runTestResponse } from "./helper"; - -import type { DockerHost } from "~/typings/docker"; - -describe("DockStatAPI (POST)", () => { - it("Check Host adding", async () => { - const body = { - name: "test", - hostAddress: "localhost:2375", - secure: false, - }; - - await runTestCode("/docker-config/add-host", 200, "POST", body); - await runTestCode("/docker-config/hosts", 200, "GET"); - }); - - it("Check Host Updating", async () => { - const codeBody: DockerHost = { - id: 2, - name: "test", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - await runTestCode("/docker-config/update-host", 200, "POST", codeBody); - - const responseBody: DockerHost[] = [ - { id: 2, name: "test", hostAddress: "127.0.0.1:2375", secure: false }, - { - id: 1, - name: "Localhost", - hostAddress: "localhost:2375", - secure: false, - }, - ]; - await runTestResponse( - "/docker-config/hosts", - JSON.stringify(responseBody), - "GET", - ); - }); - - it("Check Config update", async () => { - await runTestCode("/config/update", 200, "POST", { - fetching_interval: 1, - keep_data_for: 1, - api_key: "TestKey", - }); - }); -}); From 40127181bef0a7e551982030897c4762bf4e8485 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 9 May 2025 00:39:19 +0200 Subject: [PATCH 299/369] CI/CD: Unit test update *WIP* --- .github/workflows/ci.yml | 4 +- .gitignore | 3 +- src/core/database/dockerHosts.ts | 88 ++--- src/core/database/index.ts | 16 +- src/tests/api-config.spec.ts | 572 +++++++++++++++++-------------- src/tests/docker-manager.spec.ts | 464 +++++++++++++++++++++++++ src/tests/docker-manager.ts | 327 ------------------ src/tests/junit-exporter.ts | 72 +++- 8 files changed, 913 insertions(+), 633 deletions(-) create mode 100644 src/tests/docker-manager.spec.ts delete mode 100644 src/tests/docker-manager.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3047c175..c243ed02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,14 +52,14 @@ jobs: export PAD_NEW_LINES=false docker compose -f docker/docker-compose.dev.yaml up -d bun clean - bun test --reporter=junit --reporter-outfile=./unit-test.xml + bun test bun clean - name: Publish Test Report if: always() uses: mikepenz/action-junit-report@v5 with: - report_paths: "unit-test.xml" + report_paths: "reports/junit/*.xml" - name: Commit and push lint changes if: | diff --git a/.gitignore b/.gitignore index 1c7d1e18..78bc2da9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ build data *.xml dependency-* -Knip-Report.md \ No newline at end of file +Knip-Report.md +reports/** \ No newline at end of file diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts index 18180c54..a2fc2ca0 100644 --- a/src/core/database/dockerHosts.ts +++ b/src/core/database/dockerHosts.ts @@ -3,60 +3,60 @@ import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", - ), - selectAll: db.prepare( - "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC", - ), - update: db.prepare( - "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?", - ), - delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), + insert: db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" + ), + selectAll: db.prepare( + "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC" + ), + update: db.prepare( + "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?" + ), + delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), }; export function addDockerHost(host: DockerHost) { - return executeDbOperation( - "Add Docker Host", - () => stmt.insert.run(host.name, host.hostAddress, host.secure), - () => { - if (!host.name || !host.hostAddress) - throw new Error("Missing required fields"); - if (typeof host.secure !== "boolean") - throw new TypeError("Invalid secure type"); - }, - ); + return executeDbOperation( + "Add Docker Host", + () => stmt.insert.run(host.name, host.hostAddress, host.secure), + () => { + if (!host.name || !host.hostAddress) + throw new Error("Missing required fields"); + if (typeof host.secure !== "boolean") + throw new TypeError("Invalid secure type"); + } + ); } export function getDockerHosts(): DockerHost[] { - return executeDbOperation("Get Docker Hosts", () => { - const rows = stmt.selectAll.all() as Array< - Omit & { secure: number } - >; - return rows.map((row) => ({ - ...row, - secure: row.secure === 1, - })); - }); + return executeDbOperation("Get Docker Hosts", () => { + const rows = stmt.selectAll.all() as Array< + Omit & { secure: number } + >; + return rows.map((row) => ({ + ...row, + secure: row.secure === 1, + })); + }); } 1; export function updateDockerHost(host: DockerHost) { - return executeDbOperation( - "Update Docker Host", - () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), - () => { - if (!host.id || typeof host.id !== "number") - throw new Error("Invalid host ID"); - }, - ); + return executeDbOperation( + "Update Docker Host", + () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), + () => { + if (!host.id || typeof host.id !== "number") + throw new Error("Invalid host ID"); + } + ); } export function deleteDockerHost(id: number) { - return executeDbOperation( - "Delete Docker Host", - () => stmt.delete.run(id), - () => { - if (typeof id !== "number") throw new TypeError("Invalid ID type"); - }, - ); + return executeDbOperation( + "Delete Docker Host", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid ID type"); + } + ); } diff --git a/src/core/database/index.ts b/src/core/database/index.ts index 9158cadf..104d75f3 100644 --- a/src/core/database/index.ts +++ b/src/core/database/index.ts @@ -11,11 +11,13 @@ import * as logs from "~/core/database/logs"; import * as stacks from "~/core/database/stacks"; export const dbFunctions = { - ...dockerHosts, - ...logs, - ...config, - ...containerStats, - ...hostStats, - ...stacks, - ...backup, + ...dockerHosts, + ...logs, + ...config, + ...containerStats, + ...hostStats, + ...stacks, + ...backup, }; + +export type dbFunctions = typeof dbFunctions; diff --git a/src/tests/api-config.spec.ts b/src/tests/api-config.spec.ts index 1241885a..b97d8d41 100644 --- a/src/tests/api-config.spec.ts +++ b/src/tests/api-config.spec.ts @@ -2,269 +2,343 @@ import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; import { Elysia } from "elysia"; import { logger } from "~/core/utils/logger"; import { apiConfigRoutes } from "~/routes/api-config"; -import { - generateJunitReport, - recordTestResult, - testResults, -} from "./junit-exporter"; +import { generateJunitReport, recordTestResult } from "./junit-exporter"; +import type { TestContext } from "./junit-exporter"; const mockDb = { - getConfig: mock(() => [ - { - fetching_interval: 10, - keep_data_for: 14, - api_key: "$argon2id$v=19$m=65536,t=2,p=1$...", - }, - ]), - updateConfig: mock(), - backupDatabase: mock( - () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak`, - ), - restoreDatabase: mock(), - findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), + updateConfig: mock(() => ({})), + backupDatabase: mock( + () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak` + ), + restoreDatabase: mock(), + findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), }; mock.module("node:fs", () => ({ - existsSync: mock((path) => path.includes("dockstatapi")), - readdirSync: mock(() => [ - "dockstatapi-2025-05-06.db.bak", - "dockstatapi.db", - "dockstatapi.db-shm", - ]), - unlinkSync: mock(), + existsSync: mock((path) => path.includes("dockstatapi")), + readdirSync: mock(() => [ + "dockstatapi-2025-05-06.db.bak", + "dockstatapi.db", + "dockstatapi.db-shm", + ]), + unlinkSync: mock(), })); const mockPlugins = [ - { - name: "docker-monitor", - version: "1.2.0", - status: "active", - }, + { + name: "docker-monitor", + version: "1.2.0", + status: "active", + }, ]; const createTestApp = () => - new Elysia().use(apiConfigRoutes).decorate({ - db: mockDb, - pluginManager: { - getLoadedPlugins: mock(() => mockPlugins), - getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), - }, - logger: { - ...logger, - debug: mock(), - error: mock(), - info: mock(), - }, - }); + new Elysia().use(apiConfigRoutes).decorate({ + dbFunctions: mockDb, + pluginManager: { + getLoadedPlugins: mock(() => mockPlugins), + getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), + }, + logger: { + ...logger, + debug: mock(), + error: mock(), + info: mock(), + }, + }); + +async function captureTestContext( + req: Request, + res: Response +): Promise { + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: string; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch (textError) { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; +} describe("API Configuration Endpoints", () => { - beforeEach(() => { - mockDb.getConfig.mockClear(); - mockDb.updateConfig.mockClear(); - }); - - describe("Core Configuration", () => { - it("should retrieve current config with hashed API key", async () => { - const start = Date.now(); - try { - const app = createTestApp(); - const res = await app.handle( - new Request("http://localhost:3000/config"), - ); - - expect(res.status).toBe(200); - const data = await res.json(); - expect(data).toMatchObject({ - fetching_interval: expect.any(Number), - keep_data_for: expect.any(Number), - }); - - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - - it("should handle config update with valid payload", async () => { - const start = Date.now(); - try { - const app = createTestApp(); - const res = await app.handle( - new Request("http://localhost:3000/config/update", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - fetching_interval: 15, - keep_data_for: 30, - api_key: "new-valid-key", - }), - }), - ); - - expect(res.status).toBe(200); - expect(await res.json()).toMatchObject({ - success: true, - message: expect.stringContaining("Updated"), - }); - - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); - - describe("Plugin Management", () => { - it("should list active plugins with metadata", async () => { - const start = Date.now(); - try { - const app = createTestApp(); - const res = await app.handle( - new Request("http://localhost:3000/config/plugins"), - ); - - expect(res.status).toBe(200); - expect(await res.json()).toEqual([]); - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); - - describe("Backup Management", () => { - it("should generate timestamped backup files", async () => { - const start = Date.now(); - try { - const app = createTestApp(); - const res = await app.handle( - new Request("http://localhost:3000/config/backup", { - method: "POST", - }), - ); - - expect(res.status).toBe(200); - const { message } = await res.json(); - expect(message).toMatch( - /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/, - ); - - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - - it("should list valid backup files", async () => { - const start = Date.now(); - try { - const app = createTestApp(); - const res = await app.handle( - new Request("http://localhost:3000/config/backup"), - ); - - expect(res.status).toBe(200); - const backups = await res.json(); - expect(backups).toEqual( - expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]), - ); - - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); - - describe("Error Handling", () => { - it("should return proper error format", async () => { - const start = Date.now(); - try { - mockDb.getConfig.mockImplementationOnce(() => { - throw new Error("Database connection failed"); - }); - - const app = createTestApp(); - const res = await app.handle( - new Request("http://localhost:3000/config"), - ); - - expect(res.status).toBe(200); - const data = await res.json(); - expect(data).toMatchObject({ - api_key: expect.stringMatching(/^\$argon2id\$/), - fetching_interval: 15, - keep_data_for: 30, - }); - - recordTestResult({ - name: "should return proper error format", - suite: "API Configuration Endpoints - Error Handling", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "should return proper error format", - suite: "API Configuration Endpoints - Error Handling", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.updateConfig.mockClear(); + }); + + describe("Core Configuration", () => { + it("should retrieve current config with hashed API key", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + fetching_interval: expect.any(Number), + keep_data_for: expect.any(Number), + }); + + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with valid config structure", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle config update with valid payload", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const requestBody = { + fetching_interval: 15, + keep_data_for: 30, + api_key: "new-valid-key", + }; + const req = new Request("http://localhost:3000/config/update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestBody), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + success: true, + message: expect.stringContaining("Updated"), + }); + + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Plugin Management", () => { + it("should list active plugins with metadata", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/plugins"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual( + [] + //expect.arrayContaining([ + // expect.objectContaining({ + // name: expect.any(String), + // version: expect.any(String), + // status: expect.any(String), + // }), + //]) + ); + + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with plugin list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Backup Management", () => { + it("should generate timestamped backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup", { + method: "POST", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const { message } = context.response.body as { message: string }; + expect(message).toMatch( + /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/ + ); + + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup path", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should list valid backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const backups = context.response.body as string[]; + expect(backups).toEqual( + expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]) + ); + + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Error Handling", () => { + it("should return proper error format", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/random_link", { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(404); + + recordTestResult({ + name: "should return proper error format", + suite: + "API Configuration Endpoints - Error Handling of unkown routes", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should return proper error format", + suite: "API Configuration Endpoints - Error Handling", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "500 Error with structured error format", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateJunitReport(); + generateJunitReport(); }); diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts new file mode 100644 index 00000000..b8864e8a --- /dev/null +++ b/src/tests/docker-manager.spec.ts @@ -0,0 +1,464 @@ +import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; +import { Elysia } from "elysia"; +import { dbFunctions } from "~/core/database"; +import { dockerRoutes } from "~/routes/docker-manager"; +import { + generateJunitReport, + recordTestResult, + testResults, +} from "./junit-exporter"; +import type { TestContext } from "./junit-exporter"; + +type DockerHost = { + id?: number; + name: string; + hostAddress: string; + secure: boolean; +}; + +const mockDb = { + addDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + updateDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + getDockerHosts: mock(() => []), + deleteDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), +}; + +mock.module("~/core/database", () => ({ + dbFunctions: mockDb, +})); + +mock.module("~/core/utils/logger", () => ({ + logger: { + debug: mock(), + info: mock(), + error: mock(), + }, +})); + +const createApp = () => new Elysia().use(dockerRoutes).decorate({}); + +async function captureTestContext( + req: Request, + res: Response +): Promise { + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: unknown; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; +} + +describe("Docker Configuration Endpoints", () => { + beforeEach(() => { + mockDb.addDockerHost.mockClear(); + mockDb.updateDockerHost.mockClear(); + mockDb.getDockerHosts.mockClear(); + mockDb.deleteDockerHost.mockClear(); + }); + + describe("POST /docker-config/add-host", () => { + it("should add a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host1", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Added docker host (${host.name})`, + }); + expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with success message", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when adding a docker host fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host2", + hostAddress: "invalid", + secure: true, + }; + + // Set mock implementation + mockDb.addDockerHost.mockImplementationOnce(() => { + throw new Error("DB error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error structure", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("POST /docker-config/update-host", () => { + it("should update a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 1, + name: "Host1-upd", + hostAddress: "127.0.0.1:2376", + secure: true, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Updated docker host (${host.id})`, + }); + expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when update fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 2, + name: "Host2", + hostAddress: "x", + secure: false, + }; + + mockDb.updateDockerHost.mockImplementationOnce(() => { + throw new Error("Update error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("GET /docker-config/hosts", () => { + it("should retrieve list of hosts", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const hosts: DockerHost[] = [ + { id: 1, name: "H1", hostAddress: "a", secure: false }, + ]; + + mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual(hosts); + + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with hosts array", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when retrieval fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + mockDb.getDockerHosts.mockImplementationOnce(() => { + throw new Error("Fetch error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("DELETE /docker-config/hosts/:id", () => { + it("should delete a host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 5; + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Deleted docker host (${id})`, + }); + expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); + + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with deletion confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when delete fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 6; + + mockDb.deleteDockerHost.mockImplementationOnce(() => { + throw new Error("Delete error"); + }); + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); +}); + +afterAll(() => { + generateJunitReport(); +}); diff --git a/src/tests/docker-manager.ts b/src/tests/docker-manager.ts deleted file mode 100644 index 4c95916e..00000000 --- a/src/tests/docker-manager.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; -import { Elysia } from "elysia"; -import { dbFunctions } from "~/core/database"; -import { dockerRoutes } from "~/routes/docker-manager"; -import { - generateJunitReport, - recordTestResult, - testResults, -} from "./junit-exporter"; - -type DockerHost = { - id?: number; - name: string; - hostAddress: string; - secure: boolean; -}; - -mock.module("~/core/database", () => ({ - dbFunctions: { - addDockerHost: mock(), - updateDockerHost: mock(), - getDockerHosts: mock(), - deleteDockerHost: mock(), - }, -})); - -// Silence logger -mock.module("~/core/utils/logger", () => ({ - logger: { debug: mock(), info: mock(), error: mock() }, -})); - -const createApp = () => new Elysia().use(dockerRoutes).decorate({}); - -describe("Docker Configuration Endpoints", () => { - beforeEach(() => { - // Clear mocks and testResults - testResults.length = 0; - Object.values(dbFunctions).forEach((fn) => fn.mockClear()); - }); - - describe("POST /docker-config/add-host", () => { - it("should add a docker host successfully", async () => { - const start = Date.now(); - const host: DockerHost = { - name: "Host1", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - try { - const app = createApp(); - const res = await app.handle( - new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }), - ); - expect(res.status).toBe(200); - const data = await res.json(); - expect(data).toMatchObject({ - message: `Added docker host (${host.name})`, - }); - expect(dbFunctions.addDockerHost).toHaveBeenCalledWith(host); - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - - it("should handle error when adding a docker host fails", async () => { - const start = Date.now(); - const host: DockerHost = { - name: "Host2", - hostAddress: "invalid", - secure: true, - }; - dbFunctions.addDockerHost.mockImplementationOnce(() => { - throw new Error("DB error"); - }); - try { - const app = createApp(); - const res = await app.handle( - new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }), - ); - expect(res.status).toBe(400); - const data = await res.json(); - expect(data).toHaveProperty("error"); - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); - - describe("POST /docker-config/update-host", () => { - it("should update a docker host successfully", async () => { - const start = Date.now(); - const host: DockerHost = { - id: 1, - name: "Host1-upd", - hostAddress: "127.0.0.1:2376", - secure: true, - }; - try { - const app = createApp(); - const res = await app.handle( - new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }), - ); - expect(res.status).toBe(200); - const data = await res.json(); - expect(data).toMatchObject({ - message: `Updated docker host (${host.id})`, - }); - expect(dbFunctions.updateDockerHost).toHaveBeenCalledWith(host); - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - - it("should handle error when update fails", async () => { - const start = Date.now(); - const host: DockerHost = { - id: 2, - name: "Host2", - hostAddress: "x", - secure: false, - }; - dbFunctions.updateDockerHost.mockImplementationOnce(() => { - throw new Error("Update error"); - }); - try { - const app = createApp(); - const res = await app.handle( - new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }), - ); - expect(res.status).toBe(400); - const data = await res.json(); - expect(data).toHaveProperty("error"); - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); - - describe("GET /docker-config/hosts", () => { - it("should retrieve list of hosts", async () => { - const start = Date.now(); - const hosts: DockerHost[] = [ - { id: 1, name: "H1", hostAddress: "a", secure: false }, - ]; - dbFunctions.getDockerHosts.mockReturnValueOnce(hosts); - try { - const app = createApp(); - const res = await app.handle( - new Request("http://localhost/docker-config/hosts"), - ); - expect(res.status).toBe(200); - const data = await res.json(); - expect(data).toEqual(hosts); - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - - it("should handle error when retrieval fails", async () => { - const start = Date.now(); - dbFunctions.getDockerHosts.mockImplementationOnce(() => { - throw new Error("Fetch error"); - }); - try { - const app = createApp(); - const res = await app.handle( - new Request("http://localhost/docker-config/hosts"), - ); - expect(res.status).toBe(400); - const data = await res.json(); - expect(data).toHaveProperty("error"); - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); - - describe("DELETE /docker-config/hosts/:id", () => { - it("should delete a host successfully", async () => { - const start = Date.now(); - const id = 5; - try { - const app = createApp(); - const res = await app.handle( - new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }), - ); - expect(res.status).toBe(200); - const data = await res.json(); - expect(data).toMatchObject({ message: `Deleted docker host (${id})` }); - expect(dbFunctions.deleteDockerHost).toHaveBeenCalledWith(id); - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - - it("should handle error when delete fails", async () => { - const start = Date.now(); - const id = 6; - dbFunctions.deleteDockerHost.mockImplementationOnce(() => { - throw new Error("Delete error"); - }); - try { - const app = createApp(); - const res = await app.handle( - new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }), - ); - expect(res.status).toBe(400); - const data = await res.json(); - expect(data).toHaveProperty("error"); - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - }); - } catch (error) { - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - }); - throw error; - } - }); - }); -}); - -afterAll(() => { - generateJunitReport(); -}); diff --git a/src/tests/junit-exporter.ts b/src/tests/junit-exporter.ts index ae7b78f6..0ff4bd91 100644 --- a/src/tests/junit-exporter.ts +++ b/src/tests/junit-exporter.ts @@ -2,11 +2,33 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { format } from "date-fns"; import { logger } from "~/core/utils/logger"; +export type TestContext = { + request: { + method: string; + url: string; + headers: Record; + query?: Record; + body?: unknown; + }; + response: { + status: number; + headers: Record; + body?: unknown; + }; +}; + +type ErrorDetails = { + expected?: unknown; + received?: unknown; +}; + type TestResult = { name: string; suite: string; time: number; error?: Error; + context?: TestContext; + errorDetails?: ErrorDetails; }; export function recordTestResult(result: TestResult) { @@ -16,6 +38,47 @@ export function recordTestResult(result: TestResult) { export let testResults: TestResult[] = []; +function formatContext( + context?: TestContext, + errorDetails?: ErrorDetails +): string { + if (!context) return ""; + + let output = "=== REQUEST ===\n"; + output += `Method: ${context.request.method}\n`; + output += `URL: ${context.request.url}\n`; + + if (context.request.query) { + output += `Query Params: ${JSON.stringify( + context.request.query, + null, + 2 + )}\n`; + } + + output += `Headers: ${JSON.stringify(context.request.headers, null, 2)}\n`; + + if (context.request.body) { + output += `Body: ${JSON.stringify(context.request.body, null, 2)}\n`; + } + + output += "\n=== RESPONSE ===\n"; + output += `Status: ${context.response.status}\n`; + output += `Headers: ${JSON.stringify(context.response.headers, null, 2)}\n`; + + if (context.response.body) { + output += `Body: ${JSON.stringify(context.response.body, null, 2)}\n`; + } + + if (errorDetails) { + output += "\n=== ERROR DETAILS ===\n"; + output += `Expected: ${JSON.stringify(errorDetails.expected, null, 2)}\n`; + output += `Received: ${JSON.stringify(errorDetails.received, null, 2)}\n`; + } + + return output.replace(/]]>/g, "]]]]>>"); +} + export function generateJunitReport() { if (testResults.length === 0) { logger.warn("No test results to generate JUnit report."); @@ -39,9 +102,9 @@ export function generateJunitReport() { .map(([suiteName, cases]) => { const suiteErrors = cases.filter((c) => c.error).length; return ` - <testsuite name="${suiteName}" - tests="${cases.length}" - errors="${suiteErrors}" + <testsuite name="${suiteName}" + tests="${cases.length}" + errors="${suiteErrors}" timestamp="${format(new Date(), "yyyy-MM-dd'T'HH:mm:ss")}"> ${cases .map( @@ -57,6 +120,9 @@ export function generateJunitReport() { </failure>` : "" } + <system-out> + <![CDATA[${formatContext(testCase.context)} + ` ) .join("\n")} From 110152e2c3bec94aeb23cbff877deb4b16a49ad5 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 9 May 2025 00:44:01 +0200 Subject: [PATCH 300/369] CI/CD: Fix? --- .github/workflows/ci.yml | 1 - src/core/database/database.ts | 38 +++++++++++++++++------------------ 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c243ed02..50bbd1b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,6 @@ jobs: export DOCKSTATAPI_PORT=5971 export PAD_NEW_LINES=false docker compose -f docker/docker-compose.dev.yaml up -d - bun clean bun test bun clean diff --git a/src/core/database/database.ts b/src/core/database/database.ts index a8d4c13e..a471dc8f 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -1,10 +1,10 @@ import { Database } from "bun:sqlite"; - import { existsSync, mkdirSync } from "node:fs"; const dataFolder = "data"; + if (!existsSync(dataFolder)) { - mkdirSync(dataFolder, { recursive: true }); + mkdirSync(dataFolder, { recursive: true }); } const databasePath = "data/dockstatapi.db"; @@ -13,7 +13,7 @@ export const db = new Database(databasePath, { strict: true }); db.exec("PRAGMA journal_mode = WAL;"); export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -77,25 +77,25 @@ export function init() { ); `); - const configRow = db - .prepare("SELECT COUNT(*) AS count FROM config") - .get() as { count: number }; + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; - if (configRow.count === 0) { - db.prepare( - 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")', - ).run(); - } + if (configRow.count === 0) { + db.prepare( + 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")' + ).run(); + } - const hostRow = db - .prepare("SELECT COUNT(*) AS count FROM docker_hosts") - .get() as { count: number }; + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; - if (hostRow.count === 0) { - db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", - ).run("Localhost", "localhost:2375", false); - } + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" + ).run("Localhost", "localhost:2375", false); + } } init(); From af1f3e5700bf6511b879a28235deadf4a3bfe09f Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 9 May 2025 00:45:58 +0200 Subject: [PATCH 301/369] CI/CD: Remove different server port in CI --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50bbd1b1..31877af5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,6 @@ jobs: - name: Run unit tests run: | - export DOCKSTATAPI_PORT=5971 export PAD_NEW_LINES=false docker compose -f docker/docker-compose.dev.yaml up -d bun test From 343498046510b71008e7b2933e711b7995321329 Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 9 May 2025 00:50:07 +0200 Subject: [PATCH 302/369] CI/CD: This? --- src/core/database/database.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/core/database/database.ts b/src/core/database/database.ts index a471dc8f..24709fa2 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -7,10 +7,17 @@ if (!existsSync(dataFolder)) { mkdirSync(dataFolder, { recursive: true }); } -const databasePath = "data/dockstatapi.db"; -export const db = new Database(databasePath, { strict: true }); +export let db: Database; -db.exec("PRAGMA journal_mode = WAL;"); +try { + const databasePath = "data/dockstatapi.db"; + db = new Database(databasePath, { strict: true }); + db.exec("PRAGMA journal_mode = WAL;"); +} catch (error) { + console.error(`Cannot start DockStatAPI: ${error}`); + process.exit; + throw new Error(error as string); +} export function init() { db.exec(` From b29081beb4105050adfe2aaeb1d0244e8de5dadb Mon Sep 17 00:00:00 2001 From: ItsNik Date: Fri, 9 May 2025 00:54:38 +0200 Subject: [PATCH 303/369] CI/CD: istg ts works on my machine --- src/core/database/database.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/database/database.ts b/src/core/database/database.ts index 24709fa2..cf5512e0 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -3,19 +3,19 @@ import { existsSync, mkdirSync } from "node:fs"; const dataFolder = "data"; -if (!existsSync(dataFolder)) { - mkdirSync(dataFolder, { recursive: true }); -} - export let db: Database; try { - const databasePath = "data/dockstatapi.db"; - db = new Database(databasePath, { strict: true }); + const databasePath = `${dataFolder}/dockstatapi.db`; + + if (!existsSync(dataFolder)) { + mkdirSync(dataFolder, { recursive: true }); + } + + db = new Database(databasePath, { strict: true, create: true }); db.exec("PRAGMA journal_mode = WAL;"); } catch (error) { console.error(`Cannot start DockStatAPI: ${error}`); - process.exit; throw new Error(error as string); } From 805fe4ed2df57ccddbfb15f49d98b21975981589 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 11 May 2025 13:11:59 +0200 Subject: [PATCH 304/369] Feat: More resillient debugging for Database initialisation --- src/core/database/database.ts | 24 ++++++++++++++++++------ src/core/database/dockerHosts.ts | 1 - 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/core/database/database.ts b/src/core/database/database.ts index cf5512e0..83759067 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -1,22 +1,34 @@ import { Database } from "bun:sqlite"; -import { existsSync, mkdirSync } from "node:fs"; +import { existsSync } from "node:fs"; +import { mkdir } from "node:fs/promises"; +import path from "node:path"; +import { userInfo } from "node:os"; -const dataFolder = "data"; +const dataFolder = path.join(process.cwd(), "data"); + +const username = userInfo().username; +const gid = userInfo().gid; +const uid = userInfo().uid; export let db: Database; try { - const databasePath = `${dataFolder}/dockstatapi.db`; + const databasePath = path.join(dataFolder, "dockstatapi.db"); + console.log("Database path:", databasePath); + console.log(`Running as: ${username} (${uid}:${gid})`); if (!existsSync(dataFolder)) { - mkdirSync(dataFolder, { recursive: true }); + await mkdir(dataFolder, { recursive: true, mode: 0o777 }); + console.log("Created data directory:", dataFolder); } - db = new Database(databasePath, { strict: true, create: true }); + db = new Database(databasePath, { create: true }); + console.log("Database opened successfully"); + db.exec("PRAGMA journal_mode = WAL;"); } catch (error) { console.error(`Cannot start DockStatAPI: ${error}`); - throw new Error(error as string); + process.exit(500); } export function init() { diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts index a2fc2ca0..62b94859 100644 --- a/src/core/database/dockerHosts.ts +++ b/src/core/database/dockerHosts.ts @@ -1,4 +1,3 @@ -import type { DockerHost } from "~/typings/docker"; import { db } from "./database"; import { executeDbOperation } from "./helper"; From d022f7271b0154b4a1fafab2af4d9cbc77a3753f Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 11 May 2025 11:12:34 +0000 Subject: [PATCH 305/369] Update dependency graphs --- dependency-graph.mmd | 297 ++++----- dependency-graph.svg | 1435 +++++++++++++++++++++--------------------- 2 files changed, 878 insertions(+), 854 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index e54cc0b3..85674285 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -10,55 +10,55 @@ subgraph 0["src"] 1["index.ts"] subgraph 6["routes"] 7["live-stacks.ts"] -V["live-logs.ts"] -1H["api-config.ts"] -1J["docker-manager.ts"] -1K["docker-stats.ts"] -1L["docker-websocket.ts"] -1N["logs.ts"] -1O["stacks.ts"] -1R["utils.ts"] +X["live-logs.ts"] +1I["api-config.ts"] +1K["docker-manager.ts"] +1L["docker-stats.ts"] +1M["docker-websocket.ts"] +1O["logs.ts"] +1P["stacks.ts"] +1S["utils.ts"] end subgraph 9["core"] subgraph A["utils"] B["logger.ts"] -U["helpers.ts"] -15["calculations.ts"] -19["change-me-checker.ts"] -1B["package-json.ts"] -1D["swagger-readme.ts"] -1I["response-handler.ts"] +W["helpers.ts"] +17["calculations.ts"] +1B["change-me-checker.ts"] +1C["package-json.ts"] +1E["swagger-readme.ts"] +1J["response-handler.ts"] end subgraph D["database"] E["_dbState.ts"] F["index.ts"] G["backup.ts"] J["database.ts"] -L["helper.ts"] -M["config.ts"] -N["containerStats.ts"] -O["dockerHosts.ts"] -Q["hostStats.ts"] -R["logs.ts"] -S["stacks.ts"] +N["helper.ts"] +O["config.ts"] +P["containerStats.ts"] +Q["dockerHosts.ts"] +R["hostStats.ts"] +T["logs.ts"] +U["stacks.ts"] end -subgraph W["docker"] -X["monitor.ts"] -12["client.ts"] -13["scheduler.ts"] -14["store-container-stats.ts"] -16["store-host-stats.ts"] +subgraph Y["docker"] +Z["monitor.ts"] +14["client.ts"] +15["scheduler.ts"] +16["store-container-stats.ts"] +18["store-host-stats.ts"] end -subgraph Y["plugins"] -Z["plugin-manager.ts"] -18["loader.ts"] +subgraph 10["plugins"] +11["plugin-manager.ts"] +1A["loader.ts"] end -subgraph 1P["stacks"] -1Q["controller.ts"] +subgraph 1Q["stacks"] +1R["controller.ts"] end end -subgraph 1E["middleware"] -1F["auth.ts"] +subgraph 1F["middleware"] +1G["auth.ts"] end end subgraph 2["~"] @@ -66,167 +66,170 @@ subgraph 3["typings"] 4["database"] 8["websocket"] H["misc"] -P["docker"] -T["docker-compose"] -10["plugin"] -17["dockerode"] -1G["elysiajs"] +S["docker"] +V["docker-compose"] +12["plugin"] +19["dockerode"] +1H["elysiajs"] end end 5["elysia-remote-dts"] C["path"] subgraph I["fs"] -1A["promises"] +L["promises"] end K["bun:sqlite"] -11["events"] -1C["package.json"] -1M["stream"] +M["os"] +13["events"] +1D["package.json"] +1N["stream"] 1-->7 1-->F -1-->X -1-->13 -1-->18 +1-->Z +1-->15 +1-->1A 1-->B -1-->1B -1-->1D -1-->1F -1-->1H -1-->1J +1-->1C +1-->1E +1-->1G +1-->1I 1-->1K 1-->1L -1-->V -1-->1N +1-->1M +1-->X 1-->1O -1-->1R +1-->1P +1-->1S 1-->4 1-->5 7-->B 7-->8 B-->E B-->F -B-->V +B-->X B-->4 B-->C F-->G -F-->M -F-->N -F-->J F-->O +F-->P +F-->J F-->Q F-->R -F-->S +F-->T +F-->U G-->E G-->J -G-->L +G-->N G-->B G-->H G-->I J-->K J-->I -L-->E -L-->B -M-->J -M-->L -N-->J -N-->L +J-->L +J-->M +J-->C +N-->E +N-->B O-->J -O-->L -O-->P +O-->N +P-->J +P-->N Q-->J -Q-->L -Q-->P +Q-->N R-->J -R-->L -R-->4 -S-->U -S-->J -S-->L -S-->4 -S-->T -U-->B -V-->B -V-->4 -X-->Z -X-->F -X-->12 +R-->N +R-->S +T-->J +T-->N +T-->4 +U-->W +U-->J +U-->N +U-->4 +U-->V +W-->B X-->B -X-->P -Z-->B -Z-->P -Z-->10 +X-->4 Z-->11 -12-->B -12-->P -13-->F -13-->14 -13-->16 -13-->B -13-->4 +Z-->F +Z-->14 +Z-->B +Z-->S +11-->B +11-->S +11-->12 +11-->13 14-->B -14-->F -14-->12 -14-->15 -16-->F -16-->12 -16-->U +14-->S +15-->F +15-->16 +15-->18 +15-->B +15-->4 16-->B -16-->P +16-->F +16-->14 16-->17 -18-->19 +18-->F +18-->14 +18-->W 18-->B -18-->Z -18-->I -18-->C -19-->B -19-->1A -1B-->1C -1F-->F -1F-->B -1F-->4 -1F-->1G -1H-->F -1H-->G -1H-->Z -1H-->B -1H-->1B -1H-->1I -1H-->1F -1H-->4 -1H-->I +18-->S +18-->19 +1A-->1B +1A-->B +1A-->11 +1A-->I +1A-->C +1B-->B +1B-->L +1C-->1D +1G-->F +1G-->B +1G-->4 +1G-->1H +1I-->F +1I-->G +1I-->11 1I-->B +1I-->1C +1I-->1J 1I-->1G -1J-->F +1I-->4 +1I-->I 1J-->B -1J-->1I -1J-->P +1J-->1H 1K-->F -1K-->12 -1K-->15 -1K-->U 1K-->B -1K-->1I -1K-->P -1K-->17 +1K-->1J +1K-->S 1L-->F -1L-->12 -1L-->15 +1L-->14 +1L-->17 +1L-->W 1L-->B -1L-->1I -1L-->1M -1N-->F -1N-->B +1L-->1J +1L-->S +1L-->19 +1M-->F +1M-->14 +1M-->17 +1M-->B +1M-->1J +1M-->1N 1O-->F -1O-->1Q 1O-->B -1O-->1I -1Q-->U -1Q-->F -1Q-->B -1Q-->7 -1Q-->4 -1Q-->T -1Q-->1A -1R-->1B -1R-->1I +1P-->F +1P-->1R +1P-->B +1P-->1J +1R-->W +1R-->F +1R-->B +1R-->7 +1R-->4 +1R-->V +1R-->L +1S-->1C +1S-->1J diff --git a/dependency-graph.svg b/dependency-graph.svg index 6e3dd9ef..abb59e56 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,77 +4,77 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_~ - -~ + +~ cluster_~/typings - -typings + +typings bun:sqlite - -bun:sqlite + +bun:sqlite @@ -82,8 +82,8 @@ elysia-remote-dts - -elysia-remote-dts + +elysia-remote-dts @@ -91,8 +91,8 @@ events - -events + +events @@ -100,8 +100,8 @@ fs - -fs + +fs @@ -109,1366 +109,1387 @@ fs/promises - -promises + +promises - + +os + + +os + + + + + package.json - - -package.json + + +package.json - + path - - -path + + +path - + src/core/database/_dbState.ts - - -_dbState.ts + + +_dbState.ts - + src/core/database/backup.ts - - -backup.ts + + +backup.ts src/core/database/backup.ts->fs - - + + src/core/database/backup.ts->src/core/database/_dbState.ts - - + + - + src/core/database/database.ts - - -database.ts + + +database.ts src/core/database/backup.ts->src/core/database/database.ts - - + + - + src/core/database/helper.ts - - -helper.ts + + +helper.ts src/core/database/backup.ts->src/core/database/helper.ts - - - - + + + + - + src/core/utils/logger.ts - - -logger.ts + + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + - + ~/typings/misc - - -misc + + +misc src/core/database/backup.ts->~/typings/misc - - + + src/core/database/database.ts->bun:sqlite - - + + src/core/database/database.ts->fs - - + + + + + +src/core/database/database.ts->fs/promises + + + + + +src/core/database/database.ts->os + + + + + +src/core/database/database.ts->path + + - + src/core/database/helper.ts->src/core/database/_dbState.ts - - + + - + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + - + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + - + src/core/database/index.ts - - -index.ts + + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + - + ~/typings/database - - -database + + +database - + src/core/utils/logger.ts->~/typings/database - - + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + - + src/core/database/config.ts - - -config.ts + + +config.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/containerStats.ts - - -containerStats.ts + + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/dockerHosts.ts - - -dockerHosts.ts + + +dockerHosts.ts - + src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + - + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - - - - -~/typings/docker - - -docker - - - - - -src/core/database/dockerHosts.ts->~/typings/docker - - + + + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts - + src/core/database/hostStats.ts->src/core/database/database.ts - - + + - + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + + + + +~/typings/docker + + +docker + + - + src/core/database/hostStats.ts->~/typings/docker - - + + - + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/database.ts - - + + - + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + - + src/core/database/logs.ts - - -logs.ts + + +logs.ts - + src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + - + src/core/database/stacks.ts - - -stacks.ts + + +stacks.ts - + src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + - + src/core/database/logs.ts->src/core/database/database.ts - - + + - + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/logs.ts->~/typings/database - - + + - + src/core/database/stacks.ts->src/core/database/database.ts - - + + - + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/stacks.ts->~/typings/database - - + + - + src/core/utils/helpers.ts - - -helpers.ts + + +helpers.ts - + src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - + + + + - + ~/typings/docker-compose - - -docker-compose + + +docker-compose - + src/core/database/stacks.ts->~/typings/docker-compose - - + + - + src/core/utils/helpers.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/docker/client.ts - - -client.ts + + +client.ts - + src/core/docker/client.ts->src/core/utils/logger.ts - - + + - + src/core/docker/client.ts->~/typings/docker - - + + - + src/core/docker/monitor.ts - - -monitor.ts + + +monitor.ts - + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + - + src/core/docker/monitor.ts->~/typings/docker - - + + - + src/core/docker/monitor.ts->src/core/database/index.ts - - + + - + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + - + src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +plugin-manager.ts - + src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->~/typings/docker - - + + - + ~/typings/plugin - - -plugin + + +plugin - + src/core/plugins/plugin-manager.ts->~/typings/plugin - - + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + - + src/core/docker/scheduler.ts->~/typings/database - - + + - + src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - - -calculations.ts + + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-host-stats.ts->~/typings/docker - - + + - + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + ~/typings/dockerode - - -dockerode + + +dockerode - + src/core/docker/store-host-stats.ts->~/typings/dockerode - - + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts->~/typings/database - - + + - + src/core/stacks/controller.ts->src/core/utils/helpers.ts - - + + - + src/core/stacks/controller.ts->~/typings/docker-compose - - + + - + src/routes/live-stacks.ts - - -live-stacks.ts + + +live-stacks.ts - + src/core/stacks/controller.ts->src/routes/live-stacks.ts - - + + - + src/routes/live-stacks.ts->src/core/utils/logger.ts - - + + - + ~/typings/websocket - - -websocket + + +websocket - + src/routes/live-stacks.ts->~/typings/websocket - - + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->~/typings/database - - + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + ~/typings/elysiajs - - -elysiajs + + +elysiajs - + src/core/utils/response-handler.ts->~/typings/elysiajs - - + + - + src/core/utils/swagger-readme.ts - - -swagger-readme.ts + + +swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->elysia-remote-dts - - + + - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->~/typings/database - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + - + src/index.ts->src/routes/live-stacks.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + - + src/index.ts->src/core/utils/package-json.ts - - + + - + src/index.ts->src/core/utils/swagger-readme.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/routes/utils.ts - - -utils.ts + + +utils.ts - + src/index.ts->src/routes/utils.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->~/typings/database - - + + - + src/middleware/auth.ts->~/typings/elysiajs - - + + - + src/routes/api-config.ts->fs - - + + - + src/routes/api-config.ts->src/core/database/backup.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->~/typings/database - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->~/typings/docker - - + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->~/typings/docker - - + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/helpers.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->~/typings/dockerode - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + - + src/routes/utils.ts->src/core/utils/package-json.ts - - + + - + src/routes/utils.ts->src/core/utils/response-handler.ts - - + + From e8c7c2a82ca610eb602bddc1b952baebc3e0bbfe Mon Sep 17 00:00:00 2001 From: ItsNik Date: Wed, 14 May 2025 16:20:03 +0200 Subject: [PATCH 306/369] Feat: Better request logging (not saved in db), adjusted error responses, adjusted stack_config, more debug logging, typo fix --- .local-tests/stacks.md | 40 -- bun.lock | 444 ++++++++++++ package.json | 1 + src/core/database/database.ts | 71 +- src/core/database/dockerHosts.ts | 88 +-- src/core/database/index.ts | 14 +- src/core/database/stacks.ts | 23 +- src/core/docker/store-container-stats.ts | 2 +- src/core/stacks/controller.ts | 652 +++++++++-------- src/core/utils/helpers.ts | 4 +- src/index.ts | 26 +- src/middleware/auth.ts | 17 +- src/routes/api-config.ts | 66 +- src/routes/live-stacks.ts | 35 +- src/routes/stacks.ts | 77 +- src/routes/utils.ts | 123 ---- src/tests/api-config.spec.ts | 634 ++++++++--------- src/tests/docker-manager.spec.ts | 866 +++++++++++------------ tsconfig.json | 4 +- 19 files changed, 1753 insertions(+), 1434 deletions(-) delete mode 100644 .local-tests/stacks.md create mode 100644 bun.lock diff --git a/.local-tests/stacks.md b/.local-tests/stacks.md deleted file mode 100644 index 22a2c514..00000000 --- a/.local-tests/stacks.md +++ /dev/null @@ -1,40 +0,0 @@ -# Testing Stacks - -## Deployment - -### Values - -- compose_spec -- name -- version -- automatic_reboot_on_error -- isCustom -- image_updates -- source -- stack_prefix - -### JSON - -```json -{ - "compose_spec": { - "name": "Local Test", - "services": { - "nginx": { - "container_name": "Local-test-nginx", - "image": "dockerbogo/docker-nginx-hello-world", - "ports": [ - "8081:80" - ] - } - } - }, - "name": "Local-Test", - "version": 1, - "automatic_reboot_on_error": true, - "isCustom": true, - "image_updates": true, - "source": "Local", - "stack_prefix": "" -} -``` diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..f2cc38f1 --- /dev/null +++ b/bun.lock @@ -0,0 +1,444 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "dockstatapi", + "dependencies": { + "@elysiajs/server-timing": "^1.3.0", + "@elysiajs/static": "^1.3.0", + "@elysiajs/swagger": "^1.3.0", + "chalk": "^5.4.1", + "date-fns": "^4.1.0", + "docker-compose": "^1.2.0", + "dockerode": "^4.0.6", + "elysia": "latest", + "elysia-remote-dts": "^1.0.2", + "knip": "latest", + "logestic": "^1.2.4", + "split2": "^4.2.0", + "winston": "^3.17.0", + "yaml": "^2.7.1", + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/bun": "latest", + "@types/dockerode": "^3.3.38", + "@types/node": "^22.15.17", + "@types/split2": "^4.2.3", + "bun-types": "latest", + "cross-env": "^7.0.3", + "logform": "^2.7.0", + "typescript": "^5.8.3", + "wrap-ansi": "^9.0.0", + }, + }, + }, + "trustedDependencies": [ + "protobufjs", + ], + "packages": { + "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], + + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + + "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], + + "@elysiajs/server-timing": ["@elysiajs/server-timing@1.3.0", "", { "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-c5Ay0Va7gIWjJ9CawHx05UtKP6UQVkMKCFnf16eBG0G/GgUkrMMGHWD/duCBaDbeRwbbb7IwHDoaFvStWrB2IQ=="], + + "@elysiajs/static": ["@elysiajs/static@1.3.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-7mWlj2U/AZvH27IfRKqpUjDP1W9ZRldF9NmdnatFEtx0AOy7YYgyk0rt5hXrH6wPcR//2gO2Qy+k5rwswpEhJA=="], + + "@elysiajs/swagger": ["@elysiajs/swagger@1.3.0", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-0fo3FWkDRPNYpowJvLz3jBHe9bFe6gruZUyf+feKvUEEMG9ZHptO1jolSoPE0ffFw1BgN1/wMsP19p4GRXKdfg=="], + + "@grpc/grpc-js": ["@grpc/grpc-js@1.13.3", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="], + + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + + "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], + + "@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="], + + "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="], + + "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="], + + "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], + + "@types/dockerode": ["@types/dockerode@3.3.38", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-nnrcfUe2iR+RyOuz0B4bZgQwD9djQa9ADEjp7OAgBs10pYT0KSCtplJjcmBDJz0qaReX5T7GbE5i4VplvzUHvA=="], + + "@types/node": ["@types/node@22.15.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw=="], + + "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], + + "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], + + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + + "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], + + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], + + "bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="], + + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], + + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], + + "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "docker-compose": ["docker-compose@1.2.0", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-wIU1eHk3Op7dFgELRdmOYlPYS4gP8HhH1ZmZa13QZF59y0fblzFDFmKPhyc05phCy2hze9OEvNZAsoljrs+72w=="], + + "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], + + "dockerode": ["dockerode@4.0.6", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.1.2", "uuid": "^10.0.0" } }, "sha512-FbVf3Z8fY/kALB9s+P9epCpWhfi/r0N2DgYYcYpsAUlaTxPjdsitsFobnltb+lyCgAIvf9C+4PSWlTnHlJMf1w=="], + + "elysia": ["elysia@1.3.1", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-En41P6cDHcHtQ0nvfsn9ayB+8ahQJqG1nzvPX8FVZjOriFK/RtZPQBtXMfZDq/AsVIk7JFZGFEtAVEmztNJVhQ=="], + + "elysia-remote-dts": ["elysia-remote-dts@1.0.2", "", { "dependencies": { "debug": "4.4.0", "get-tsconfig": "4.10.0" }, "peerDependencies": { "elysia": ">= 1.0.0", "typescript": ">=5" } }, "sha512-ktRxKGozPDW24d3xbUS2sMLNsRHHX/a4Pgqyzv2O0X4HsDrD+agoUYL/PvYQrGJKPSc3xzvU5uvhNHFhEql6aw=="], + + "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + + "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="], + + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fd-package-json": ["fd-package-json@1.2.0", "", { "dependencies": { "walk-up-path": "^3.0.1" } }, "sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA=="], + + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + + "file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + + "formatly": ["formatly@0.2.3", "", { "dependencies": { "fd-package-json": "^1.2.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-WH01vbXEjh9L3bqn5V620xUAWs32CmK4IzWRRY6ep5zpa/mrisL4d9+pRVuETORVDTQw8OycSO1WC68PL51RaA=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + + "get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "knip": ["knip@5.55.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "enhanced-resolve": "^5.18.1", "fast-glob": "^3.3.3", "formatly": "^0.2.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-NYXjgGrXgMdabUKCP2TlBH/e83m9KnLc1VLyWHUtoRrCEJ/C15YtbafrpTvm3td+jE4VdDPgudvXT1IMtCx8lw=="], + + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "logestic": ["logestic@1.2.4", "", { "dependencies": { "chalk": "^5.3.0" }, "peerDependencies": { "elysia": "^1.1.3", "typescript": "^5.0.0" } }, "sha512-Wka/xFdKgqU6JBk8yxAUsqcUjPA/aExpcnm7KnOAxlLo1U71kuWGeEjPw8XVLZzLleTWwmRqJUb2yI5XZP+vAA=="], + + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="], + + "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], + + "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "protobufjs": ["protobufjs@7.5.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3qx3IRjR9WPQKagdwrKjO3Gu8RgQR2qqw+1KnigWhoVjFqegIj1K3bP11sGqhxrO46/XL7lekuG4jmjL+4cLsw=="], + + "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "smol-toml": ["smol-toml@1.3.4", "", {}, "sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA=="], + + "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="], + + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="], + + "strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="], + + "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], + + "tar-fs": ["tar-fs@2.1.2", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="], + + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + + "walk-up-path": ["walk-up-path@3.0.1", "", {}, "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], + + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + + "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], + + "zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="], + + "zod-validation-error": ["zod-validation-error@3.4.1", "", { "peerDependencies": { "zod": "^3.24.4" } }, "sha512-1KP64yqDPQ3rupxNv7oXhf7KdhHHgaqbKuspVoiN93TT0xrBjql+Svjkdjq/Qh/7GSMmgQs3AfvBT0heE35thw=="], + + "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], + + "@types/ssh2/@types/node": ["@types/node@18.19.100", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-ojmMP8SZBKprc3qGrGk8Ujpo80AXkrP7G2tOT4VWr5jlr5DHjsJF+emXJz+Wm0glmy4Js62oKMdZZ6B9Y+tEcA=="], + + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], + + "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + } +} diff --git a/package.json b/package.json index 8fb4fe58..15ffaf20 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "elysia": "latest", "elysia-remote-dts": "^1.0.2", "knip": "latest", + "logestic": "^1.2.4", "split2": "^4.2.0", "winston": "^3.17.0", "yaml": "^2.7.1" diff --git a/src/core/database/database.ts b/src/core/database/database.ts index 83759067..f8de7cb1 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -1,8 +1,8 @@ import { Database } from "bun:sqlite"; import { existsSync } from "node:fs"; import { mkdir } from "node:fs/promises"; -import path from "node:path"; import { userInfo } from "node:os"; +import path from "node:path"; const dataFolder = path.join(process.cwd(), "data"); @@ -13,26 +13,26 @@ const uid = userInfo().uid; export let db: Database; try { - const databasePath = path.join(dataFolder, "dockstatapi.db"); - console.log("Database path:", databasePath); - console.log(`Running as: ${username} (${uid}:${gid})`); + const databasePath = path.join(dataFolder, "dockstatapi.db"); + console.log("Database path:", databasePath); + console.log(`Running as: ${username} (${uid}:${gid})`); - if (!existsSync(dataFolder)) { - await mkdir(dataFolder, { recursive: true, mode: 0o777 }); - console.log("Created data directory:", dataFolder); - } + if (!existsSync(dataFolder)) { + await mkdir(dataFolder, { recursive: true, mode: 0o777 }); + console.log("Created data directory:", dataFolder); + } - db = new Database(databasePath, { create: true }); - console.log("Database opened successfully"); + db = new Database(databasePath, { create: true }); + console.log("Database opened successfully"); - db.exec("PRAGMA journal_mode = WAL;"); + db.exec("PRAGMA journal_mode = WAL;"); } catch (error) { - console.error(`Cannot start DockStatAPI: ${error}`); - process.exit(500); + console.error(`Cannot start DockStatAPI: ${error}`); + process.exit(500); } export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -47,10 +47,7 @@ export function init() { version INTEGER NOT NULL, custom BOOLEAN NOT NULL, source TEXT NOT NULL, - container_count INTEGER NOT NULL, - stack_prefix TEXT NOT NULL, - automatic_reboot_on_error BOOLEAN NOT NULL, - image_updates BOOLEAN NOT NULL + compose_spec TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS docker_hosts ( @@ -96,25 +93,25 @@ export function init() { ); `); - const configRow = db - .prepare("SELECT COUNT(*) AS count FROM config") - .get() as { count: number }; - - if (configRow.count === 0) { - db.prepare( - 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")' - ).run(); - } - - const hostRow = db - .prepare("SELECT COUNT(*) AS count FROM docker_hosts") - .get() as { count: number }; - - if (hostRow.count === 0) { - db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" - ).run("Localhost", "localhost:2375", false); - } + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; + + if (configRow.count === 0) { + db.prepare( + 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")', + ).run(); + } + + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; + + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ).run("Localhost", "localhost:2375", false); + } } init(); diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts index 62b94859..2c9903db 100644 --- a/src/core/database/dockerHosts.ts +++ b/src/core/database/dockerHosts.ts @@ -2,60 +2,60 @@ import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" - ), - selectAll: db.prepare( - "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC" - ), - update: db.prepare( - "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?" - ), - delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), + insert: db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ), + selectAll: db.prepare( + "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC", + ), + update: db.prepare( + "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?", + ), + delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), }; export function addDockerHost(host: DockerHost) { - return executeDbOperation( - "Add Docker Host", - () => stmt.insert.run(host.name, host.hostAddress, host.secure), - () => { - if (!host.name || !host.hostAddress) - throw new Error("Missing required fields"); - if (typeof host.secure !== "boolean") - throw new TypeError("Invalid secure type"); - } - ); + return executeDbOperation( + "Add Docker Host", + () => stmt.insert.run(host.name, host.hostAddress, host.secure), + () => { + if (!host.name || !host.hostAddress) + throw new Error("Missing required fields"); + if (typeof host.secure !== "boolean") + throw new TypeError("Invalid secure type"); + }, + ); } export function getDockerHosts(): DockerHost[] { - return executeDbOperation("Get Docker Hosts", () => { - const rows = stmt.selectAll.all() as Array< - Omit & { secure: number } - >; - return rows.map((row) => ({ - ...row, - secure: row.secure === 1, - })); - }); + return executeDbOperation("Get Docker Hosts", () => { + const rows = stmt.selectAll.all() as Array< + Omit & { secure: number } + >; + return rows.map((row) => ({ + ...row, + secure: row.secure === 1, + })); + }); } 1; export function updateDockerHost(host: DockerHost) { - return executeDbOperation( - "Update Docker Host", - () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), - () => { - if (!host.id || typeof host.id !== "number") - throw new Error("Invalid host ID"); - } - ); + return executeDbOperation( + "Update Docker Host", + () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), + () => { + if (!host.id || typeof host.id !== "number") + throw new Error("Invalid host ID"); + }, + ); } export function deleteDockerHost(id: number) { - return executeDbOperation( - "Delete Docker Host", - () => stmt.delete.run(id), - () => { - if (typeof id !== "number") throw new TypeError("Invalid ID type"); - } - ); + return executeDbOperation( + "Delete Docker Host", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid ID type"); + }, + ); } diff --git a/src/core/database/index.ts b/src/core/database/index.ts index 104d75f3..c381e7a6 100644 --- a/src/core/database/index.ts +++ b/src/core/database/index.ts @@ -11,13 +11,13 @@ import * as logs from "~/core/database/logs"; import * as stacks from "~/core/database/stacks"; export const dbFunctions = { - ...dockerHosts, - ...logs, - ...config, - ...containerStats, - ...hostStats, - ...stacks, - ...backup, + ...dockerHosts, + ...logs, + ...config, + ...containerStats, + ...hostStats, + ...stacks, + ...backup, }; export type dbFunctions = typeof dbFunctions; diff --git a/src/core/database/stacks.ts b/src/core/database/stacks.ts index f39d01a6..1c81e6d9 100644 --- a/src/core/database/stacks.ts +++ b/src/core/database/stacks.ts @@ -7,20 +7,17 @@ import { executeDbOperation } from "./helper"; const stmt = { insert: db.prepare(` INSERT INTO stacks_config ( - name, version, custom, source, container_count, - stack_prefix, automatic_reboot_on_error, image_updates - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + name, version, custom, source, compose_spec + ) VALUES (?, ?, ?, ?, ?) `), selectAll: db.prepare(` - SELECT id, name, version, custom, source, container_count, stack_prefix, - automatic_reboot_on_error, image_updates + SELECT id, name, version, custom, source, compose_spec FROM stacks_config ORDER BY name DESC `), update: db.prepare(` - UPDATE stacks_config SET - version = ?, custom = ?, source = ?, container_count = ?, - stack_prefix = ?, automatic_reboot_on_error = ?, image_updates = ? + UPDATE stacks_config + SET name = ?, custom = ?, source = ?, compose_spec = ? WHERE name = ? `), delete: db.prepare("DELETE FROM stacks_config WHERE id = ?"), @@ -33,10 +30,7 @@ export function addStack(stack: stacks_config) { stack.version, stack.custom, stack.source, - stack.container_count, - stack.stack_prefix, - stack.automatic_reboot_on_error, - stack.image_updates, + stack.compose_spec, ), ); @@ -65,11 +59,8 @@ export function updateStack(stack: stacks_config) { stack.version, stack.custom, stack.source, - stack.container_count, - stack.stack_prefix, - stack.automatic_reboot_on_error, - stack.image_updates, stack.name, + stack.compose_spec, ), ); } diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts index 97e0bd99..33b9c0fb 100644 --- a/src/core/docker/store-container-stats.ts +++ b/src/core/docker/store-container-stats.ts @@ -10,7 +10,7 @@ import { logger } from "../utils/logger"; async function storeContainerData() { try { const hosts = dbFunctions.getDockerHosts(); - logger.debug("Retrieved docker hosts for storring container data"); + logger.debug("Retrieved docker hosts for storing container data"); // Process each host concurrently and wait for them all to finish await Promise.all( diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index b6f6ddde..95a6480a 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -9,339 +9,393 @@ import type { ComposeSpec, Stack } from "~/typings/docker-compose"; import { findObjectByKey } from "../utils/helpers"; const wrapProgressCallback = (progressCallback?: (log: string) => void) => { - return progressCallback - ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { - const log = chunk.toString(); - progressCallback(log); - } - : undefined; + return progressCallback + ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; }; async function getStackName(stack_id: number): Promise { - logger.debug(`Fetching stack name for id ${stack_id}`); - const stacks = dbFunctions.getStacks(); - const stack = findObjectByKey(stacks, "id", stack_id); - if (!stack) { - throw new Error(`Stack with id ${stack_id} not found`); - } - return stack.name; + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = findObjectByKey(stacks, "id", stack_id); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; } async function runStackCommand( - stack_id: number, - command: ( - cwd: string, - progressCallback?: (log: string) => void, - ) => Promise, - action: string, + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void + ) => Promise, + action: string ): Promise { - try { - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - - const progressCallback = (log: string) => { - postToClient({ - type: "stack-progress", - data: { - stack_id, - action, - message: log.trim(), - timestamp: new Date().toISOString(), - }, - }); - }; - - return await command(stackPath, progressCallback); - } catch (error) { - postToClient({ - type: "stack-error", - data: { - stack_id, - action, - message: String(error), - timestamp: new Date().toISOString(), - }, - }); - throw new Error( - `Error while ${action} stack "${stack_id}": ${String(error)}`, - ); - } + try { + logger.debug( + `Starting runStackCommand for stack_id=${stack_id}, action="${action}"` + ); + + const stackName = await getStackName(stack_id); + logger.debug( + `Retrieved stack name "${stackName}" for stack_id=${stack_id}` + ); + + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); + + const progressCallback = (log: string) => { + const message = log.trim(); + logger.debug( + `Progress for stack_id=${stack_id}, action="${action}": ${message}` + ); + + // ERROR HANDLING FOR COMPOSE ACTIONS + if (message.includes("Error response from daemon")) { + logger.error( + `Error response from daemon: ${ + message.split("Error response from daemon:")[1] + }` + ); + } + + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message, + timestamp: new Date().toISOString(), + }, + }); + }; + + logger.debug( + `Executing command for stack_id=${stack_id}, action="${action}"` + ); + const result = await command(stackPath, progressCallback); + logger.debug( + `Successfully completed command for stack_id=${stack_id}, action="${action}"` + ); + + return result; + } catch (error) { + logger.debug( + `Error occurred for stack_id=${stack_id}, action="${action}": ${String( + error + )}` + ); + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: String(error), + timestamp: new Date().toISOString(), + }, + }); + throw new Error( + `Error while ${action} stack "${stack_id}": ${String(error)}` + ); + } } async function getStackPath(stack: Stack): Promise { - const stackName = stack.name.trim().replace(/\s+/g, "_"); - return `stacks/${stackName}`; + const stackName = stack.name.trim().replace(/\s+/g, "_"); + const stackId = stack.id; + + if (!stackId) { + logger.error("Stack could not be parsed"); + throw new Error("Stack could not be parsed"); + } + + return `stacks/${stackId}-${stackName}`; } async function createStackYAML(compose_spec: Stack): Promise { - const yaml = YAML.stringify(compose_spec.compose_spec); - const stackPath = await getStackPath(compose_spec); - await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { - createPath: true, - }); + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); } -export async function deployStack( - stack: ComposeSpec, - name: string, - version: number, - source: string, - automatic_reboot_on_error: boolean, - isCustom: boolean, - image_updates: boolean, - stack_prefix?: string, -): Promise { - let stackId: number; - - try { - logger.debug(`Deploying Stack: ${JSON.stringify(stack)}`); - const serviceCount = stack.services - ? Object.keys(stack.services).length - : 0; - const resolvedPrefix = stack_prefix ?? ""; - - const stack_config: stacks_config = { - id: 0, - name, - version, - source, - stack_prefix: resolvedPrefix, - automatic_reboot_on_error, - container_count: serviceCount, - custom: isCustom, - image_updates, - }; - - if (!name) { - throw new Error("Stack name needed"); - } - - stackId = dbFunctions.addStack(stack_config) as number; - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "pending", - message: "Creating stack configuration", - }, - }); - - const stackYaml: Stack = { - id: stackId, - name, - source, - version, - compose_spec: stack, - }; - - await createStackYAML(stackYaml); - - await runStackCommand( - stackId, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "deploying", - ); - - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "deployed", - message: "Stack deployed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id: 0, - action: "deploying", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } +export async function deployStack(stack_config: stacks_config): Promise { + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); + + if (!stack_config.name) { + throw new Error("Stack name needed"); + } + + const jsonStringStack = { + ...stack_config, + compose_spec: JSON.stringify(stack_config.compose_spec), + }; + + const stackId = dbFunctions.addStack(jsonStringStack); + + if (!stackId) { + throw new Error("Failed to add stack to database"); + } + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }); + + const stackYaml: Stack = { + id: stackId, + name: stack_config.name, + source: stack_config.source, + version: stack_config.version, + compose_spec: stack_config.compose_spec as unknown as ComposeSpec, // Weird stuff i am doing here... smh + }; + + await createStackYAML(stackYaml); + + await runStackCommand( + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying" + ); + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id: 0, + action: "deploying", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } export async function stopStack(stack_id: number): Promise { - // Note the await to discard the result (convert to void) - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.downAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "stopping", - ); + // Note the await to discard the result (convert to void) + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.downAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping" + ); } export async function startStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "starting", - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting" + ); } export async function pullStackImages(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.pullAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "pulling-images", - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images" + ); } export async function restartStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.restartAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "restarting", - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting" + ); } export async function getStackStatus( - stack_id: number, - //biome-ignore lint/suspicious/noExplicitAny: + stack_id: number + //biome-ignore lint/suspicious/noExplicitAny: ): Promise> { - const status = await runStackCommand( - stack_id, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - //biome-ignore lint/suspicious/noExplicitAny: - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check", - ); - return status; + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check" + ); + return status; } export async function removeStack(stack_id: number): Promise { - try { - await runStackCommand( - stack_id, - async (cwd, progressCallback) => { - await DockerCompose.down({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }); - }, - "removing", - ); - - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - - try { - await rm(stackPath, { recursive: true }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } - - dbFunctions.deleteStack(stack_id); - postToClient({ - type: "stack-removed", - data: { - stack_id, - message: "Stack removed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + try { + const _ = dbFunctions.deleteStack(stack_id); + + await runStackCommand( + stack_id, + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }); + }, + "removing" + ); + + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + + try { + await rm(stackPath, { recursive: true }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } + + postToClient({ + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } -//biome-ignore lint/suspicious/noExplicitAny: -export async function getAllStacksStatus(): Promise> { - try { - const stacks = dbFunctions.getStacks(); - - const statusResults = await Promise.all( - stacks.map(async (stack) => { - const status = await runStackCommand( - stack.id as number, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - //biome-ignore lint/suspicious/noExplicitAny: - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check", - ); - return { stackId: stack.id, status }; - }), - ); - - return statusResults.reduce( - (acc, { stackId, status }) => { - // Ensure stackId is used as a string if necessary, e.g. - acc[String(stackId)] = status; - return acc; - }, - //biome-ignore lint/suspicious/noExplicitAny: - {} as Record, - ); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } +interface DockerServiceStatus { + status: string; + ports: string[]; +} + +interface StackStatus { + services: Record; + healthy: number; + unhealthy: number; + total: number; +} + +type StacksStatus = Record; + +export async function getAllStacksStatus(): Promise { + try { + const stacks = dbFunctions.getStacks(); + + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.id as number, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + const services = rawStatus.data.services.reduce( + (acc: Record, service) => { + acc[service.name] = { + status: service.state, + ports: service.ports.map( + (port) => `${port.mapped?.address}:${port.mapped?.port}` + ), + }; + return acc; + }, + {} + ); + + const statusValues = Object.values(services); + return { + services, + healthy: statusValues.filter( + (s) => s.status === "running" || s.status.includes("Up") + ).length, + unhealthy: statusValues.filter( + (s) => s.status !== "running" && !s.status.includes("Up") + ).length, + total: statusValues.length, + }; + }, + "status-check" + ); + return { stackId: stack.id, status }; + }) + ); + + return statusResults.reduce((acc, { stackId, status }) => { + acc[String(stackId)] = status; + return acc; + }, {} as StacksStatus); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts index ab13dd40..6c3e79e6 100644 --- a/src/core/utils/helpers.ts +++ b/src/core/utils/helpers.ts @@ -5,7 +5,9 @@ export function findObjectByKey( key: keyof T, value: T[keyof T], ): T | undefined { - logger.debug(`Searching ${String(key)}`); + logger.debug( + `Searching for key: ${String(key)} with value: ${String(value)}`, + ); const data = array.find((item) => item[key] === value); return data; } diff --git a/src/index.ts b/src/index.ts index 419c6bf3..e52e8a71 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import staticPlugin from "@elysiajs/static"; import { swagger } from "@elysiajs/swagger"; import { Elysia } from "elysia"; import { dts } from "elysia-remote-dts"; +import { Logestic } from "logestic"; import { dbFunctions } from "~/core/database"; import { monitorDockerEvents } from "~/core/docker/monitor"; import { setSchedules } from "~/core/docker/scheduler"; @@ -22,7 +23,6 @@ import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; import { liveLogs } from "~/routes/live-logs"; import { backendLogs } from "~/routes/logs"; import { stackRoutes } from "~/routes/stacks"; -import { utilRoutes } from "~/routes/utils"; import type { config } from "~/typings/database"; import { liveStacks } from "./routes/live-stacks"; @@ -30,7 +30,11 @@ console.log(""); logger.info("Starting DockStatAPI"); -const DockStatAPI = new Elysia() +const DockStatAPI = new Elysia({ + normalize: true, + precompile: true, +}) + .use(Logestic.preset("fancy")) .use(staticPlugin()) .use(serverTiming()) .use( @@ -92,7 +96,7 @@ const DockStatAPI = new Elysia() if ( path === "/health" || path.startsWith("/swagger") || - path.startsWith("/trpc") + path.startsWith("/public") ) { logger.info(`Requested unguarded route: ${path}`); return; @@ -100,26 +104,34 @@ const DockStatAPI = new Elysia() const validation = await validateApiKey(request, set); - if (validation.error) { + if (!validation) { + throw new Error("Error while checking API key"); + } + + if (!validation.success) { set.status = 400; - return { error: validation.error }; + throw new Error(validation.error); } }) - .onError(({ code, set, path }) => { + .onError(({ code, set, path, error }) => { if (code === "NOT_FOUND") { logger.warn(`Unknown route (${path}), showing error page!`); set.status = 404; set.headers["Content-Type"] = "text/html"; return Bun.file("public/404.html"); } + + logger.error(`Internal server error at ${path}: ${error.message}`); + set.status = 500; + set.headers["Content-Type"] = "text/html"; + return { success: false, message: error.message }; }) .use(dockerRoutes) .use(dockerStatsRoutes) .use(backendLogs) .use(dockerWebsocketRoutes) .use(apiConfigRoutes) - .use(utilRoutes) .use(stackRoutes) .use(liveLogs) .use(liveStacks) diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 00077932..3a730229 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -47,13 +47,13 @@ export async function validateApiKey(request: Request, set: set) { logger.warn( "API Key validation deactivated, since running in development mode", ); - return { apiKey }; + return { success: true, apiKey }; } if (!apiKey) { logger.error(`API key missing from request ${request.url}`); set.status = 401; - return { error: "API key required" }; + return { error: "API key required", success: false, apiKey }; } logger.debug("API key validation initiated"); @@ -64,7 +64,12 @@ export async function validateApiKey(request: Request, set: set) { if (!dbRecord) { logger.error("API key not found in database"); set.status = 401; - return { error: "Invalid API key" }; + return { success: false, error: "Invalid API key" }; + } + + if (dbRecord.hash === "changeme") { + logger.error("Please change your API Key!"); + return { success: true, apiKey }; } const isValid = await validateApiKeyHash(apiKey, dbRecord.hash); @@ -72,13 +77,13 @@ export async function validateApiKey(request: Request, set: set) { if (!isValid) { logger.error("Invalid API key provided"); set.status = 401; - return { error: "Invalid API key" }; + return { success: false, error: "Invalid API key", apiKey }; } - return logger.info("Valid API key used"); + logger.info("Valid API key used"); } catch (error) { logger.error("Error during API key validation", error); set.status = 500; - return { error: "Internal server error" }; + return { success: false, error: "Internal server error", apiKey }; } } diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 7e51d8b3..8759505a 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -32,11 +32,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) logger.debug("Fetched backend config"); return distinct; } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting the DockStatAPI config", - ); + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { @@ -95,11 +92,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) try { return pluginManager.getLoadedPlugins(); } catch (error) { - return responseHandler.error( - set, - error as string, - "Error getting all registered plugins", - ); + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { @@ -168,11 +162,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) ); return responseHandler.ok(set, "Updated DockStatAPI config"); } catch (error) { - return responseHandler.error( - set, - "Error updating the DockStatAPI config", - error as string, - ); + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { @@ -224,10 +215,10 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) ) .get( "/package", - async ({ set }) => { + async () => { try { logger.debug("Fetching package.json"); - return { + const data = { version: version, description: description, license: license, @@ -238,12 +229,19 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) dependencies: dependencies, devDependencies: devDependencies, }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Error while reading package.json", + + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, ); + + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } + + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { @@ -337,7 +335,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) const backupFilename = await dbFunctions.backupDatabase(); return responseHandler.ok(set, backupFilename); } catch (error) { - return responseHandler.error(set, error as string, "Error backing up"); + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { @@ -397,11 +396,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return filteredFiles; } catch (error) { - return responseHandler.error( - set, - error as string, - "Reading Backup directory", - ); + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { @@ -463,11 +459,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) `attachment; filename="${filename}"`; return Bun.file(filePath); } catch (error) { - return responseHandler.error( - set, - error as string, - "Backup download failed", - ); + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { @@ -545,11 +538,8 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) return responseHandler.ok(set, "Database restored successfully"); } catch (error) { - return responseHandler.error( - set, - error instanceof Error ? error.message : "Restoration failed", - "Database restoration error", - ); + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); } }, { diff --git a/src/routes/live-stacks.ts b/src/routes/live-stacks.ts index b3a14e74..2b5e8e3a 100644 --- a/src/routes/live-stacks.ts +++ b/src/routes/live-stacks.ts @@ -1,6 +1,5 @@ import { Elysia } from "elysia"; import type { ElysiaWS } from "elysia/dist/ws"; - import { logger } from "~/core/utils/logger"; import type { stackSocketMessage } from "~/typings/websocket"; @@ -8,24 +7,24 @@ import type { stackSocketMessage } from "~/typings/websocket"; const activeConnections = new Set>(); export const liveStacks = new Elysia().ws("/stacks", { - open(ws) { - activeConnections.add(ws); - ws.send({ message: "Connection established" }); - logger.info(`New Stacks WebSocket established (${ws.id})`); - }, - close(ws) { - logger.info(`Stacks WebSocket closed (${ws.id})`); - activeConnections.delete(ws); - }, + open(ws) { + activeConnections.add(ws); + ws.send({ message: "Connection established" }); + logger.info(`New Stacks WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Stacks WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, }); export function postToClient(data: stackSocketMessage) { - for (const ws of activeConnections) { - try { - ws.send(JSON.stringify(data)); - } catch (error) { - activeConnections.delete(ws); - logger.error("Failed to send to WebSocket:", error); - } - } + for (const ws of activeConnections) { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + } } diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index d5b22421..3ac2b86d 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -1,5 +1,4 @@ import { Elysia, t } from "elysia"; - import { dbFunctions } from "~/core/database"; import { deployStack, @@ -13,45 +12,14 @@ import { } from "~/core/stacks/controller"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; +import type { stacks_config } from "~/typings/database"; export const stackRoutes = new Elysia({ prefix: "/stacks" }) .post( "/deploy", async ({ set, body }) => { try { - const isCustom = body.isCustom || false; - - const image_updates = body.image_updates || false; - - const missingParams: string[] = []; - if (!body.compose_spec) { - missingParams.push("compose_spec"); - } - if (body.automatic_reboot_on_error === undefined) { - missingParams.push("automatic_reboot_on_error"); - } - if (!body.source) { - missingParams.push("source"); - } - if (!body.name) { - missingParams.push("name"); - } - - if (missingParams.length > 0) { - const errMsg = `Missing values of: ${missingParams.join("; ")}`; - return responseHandler.error(set, errMsg, errMsg); - } - - await deployStack( - body.compose_spec, - body.name, - body.version, - body.source, - body.automatic_reboot_on_error, - isCustom, - image_updates, - body.stack_prefix, - ); + await deployStack(body as stacks_config); logger.info(`Deployed Stack (${body.name})`); return responseHandler.ok( set, @@ -60,7 +28,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error(set, errorMsg, "Error deploying stack"); + return responseHandler.error( + set, + errorMsg, + "Error deploying stack, please check the server logs for more information", + ); } }, { @@ -104,14 +76,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) }, }, body: t.Object({ - compose_spec: t.Any(), name: t.String(), version: t.Number(), - automatic_reboot_on_error: t.Boolean(), - isCustom: t.Boolean(), - image_updates: t.Boolean(), + custom: t.Boolean(), source: t.String(), - stack_prefix: t.Optional(t.String()), + compose_spec: t.Any(), }), }, ) @@ -375,24 +344,41 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) "/status", async ({ set, query }) => { try { - //biome-ignore lint/suspicious/noExplicitAny: + // biome-ignore lint/suspicious/noExplicitAny: let status: Record; let res = {}; + + logger.debug("Entering stack status handler"); + logger.debug(`Request body: ${JSON.stringify(query)}`); + if (query.stackId) { + logger.debug(`Fetching status for stackId=${query.stackId}`); status = await getStackStatus(query.stackId); + logger.debug( + `Retrieved status for stackId=${query.stackId}: ${JSON.stringify(status)}`, + ); + res = responseHandler.ok( set, `Stack ${query.stackId} status retrieved successfully`, ); logger.info("Fetched Stack status"); } else { + logger.debug("Fetching status for all stacks"); status = await getAllStacksStatus(); + logger.debug( + `Retrieved status for all stacks: ${JSON.stringify(status)}`, + ); + res = responseHandler.ok(set, "Fetched all Stack's status"); logger.info("Fetched all Stack status"); } + + logger.debug("Returning response with status data"); return { ...res, status: status }; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); + logger.debug(`Error occurred while fetching stack status: ${errorMsg}`); return responseHandler.error( set, @@ -470,9 +456,11 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) }, }, }, - query: t.Object({ - stackId: t.Number(), - }), + query: t.Optional( + t.Object({ + stackId: t.Number(), + }), + ), }, ) .get( @@ -549,7 +537,6 @@ export const stackRoutes = new Elysia({ prefix: "/stacks" }) }, }, ) - .delete( "/", async ({ set, body }) => { diff --git a/src/routes/utils.ts b/src/routes/utils.ts index 591efd51..e69de29b 100644 --- a/src/routes/utils.ts +++ b/src/routes/utils.ts @@ -1,123 +0,0 @@ -import { Elysia, t } from "elysia"; - -import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, -} from "~/core/utils/package-json"; -import { responseHandler } from "~/core/utils/response-handler"; - -export const utilRoutes = new Elysia({ prefix: "/utils" }).get( - "/info", - async ({ set }) => { - try { - set.status = 200; - return { - version, - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - }; - } catch (error) { - return responseHandler.error( - set, - String(error), - "Error getting DockStatAPI information", - ); - } - }, - { - detail: { - tags: ["Utils"], - description: - "Retrieves DockStatAPI metadata including version, author information, dependencies, and licensing details", - responses: { - "200": { - description: "Successfully retrieved API information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving API information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting DockStatAPI information", - }, - }, - }, - }, - }, - }, - }, - }, - }, -); diff --git a/src/tests/api-config.spec.ts b/src/tests/api-config.spec.ts index b97d8d41..d1f9d098 100644 --- a/src/tests/api-config.spec.ts +++ b/src/tests/api-config.spec.ts @@ -6,339 +6,339 @@ import { generateJunitReport, recordTestResult } from "./junit-exporter"; import type { TestContext } from "./junit-exporter"; const mockDb = { - updateConfig: mock(() => ({})), - backupDatabase: mock( - () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak` - ), - restoreDatabase: mock(), - findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), + updateConfig: mock(() => ({})), + backupDatabase: mock( + () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak`, + ), + restoreDatabase: mock(), + findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), }; mock.module("node:fs", () => ({ - existsSync: mock((path) => path.includes("dockstatapi")), - readdirSync: mock(() => [ - "dockstatapi-2025-05-06.db.bak", - "dockstatapi.db", - "dockstatapi.db-shm", - ]), - unlinkSync: mock(), + existsSync: mock((path) => path.includes("dockstatapi")), + readdirSync: mock(() => [ + "dockstatapi-2025-05-06.db.bak", + "dockstatapi.db", + "dockstatapi.db-shm", + ]), + unlinkSync: mock(), })); const mockPlugins = [ - { - name: "docker-monitor", - version: "1.2.0", - status: "active", - }, + { + name: "docker-monitor", + version: "1.2.0", + status: "active", + }, ]; const createTestApp = () => - new Elysia().use(apiConfigRoutes).decorate({ - dbFunctions: mockDb, - pluginManager: { - getLoadedPlugins: mock(() => mockPlugins), - getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), - }, - logger: { - ...logger, - debug: mock(), - error: mock(), - info: mock(), - }, - }); + new Elysia().use(apiConfigRoutes).decorate({ + dbFunctions: mockDb, + pluginManager: { + getLoadedPlugins: mock(() => mockPlugins), + getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), + }, + logger: { + ...logger, + debug: mock(), + error: mock(), + info: mock(), + }, + }); async function captureTestContext( - req: Request, - res: Response + req: Request, + res: Response, ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: string; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch (textError) { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: string; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch (textError) { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("API Configuration Endpoints", () => { - beforeEach(() => { - mockDb.updateConfig.mockClear(); - }); - - describe("Core Configuration", () => { - it("should retrieve current config with hashed API key", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - fetching_interval: expect.any(Number), - keep_data_for: expect.any(Number), - }); - - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with valid config structure", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle config update with valid payload", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const requestBody = { - fetching_interval: 15, - keep_data_for: 30, - api_key: "new-valid-key", - }; - const req = new Request("http://localhost:3000/config/update", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(requestBody), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - success: true, - message: expect.stringContaining("Updated"), - }); - - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Plugin Management", () => { - it("should list active plugins with metadata", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/plugins"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual( - [] - //expect.arrayContaining([ - // expect.objectContaining({ - // name: expect.any(String), - // version: expect.any(String), - // status: expect.any(String), - // }), - //]) - ); - - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with plugin list", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Backup Management", () => { - it("should generate timestamped backup files", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/backup", { - method: "POST", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - const { message } = context.response.body as { message: string }; - expect(message).toMatch( - /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/ - ); - - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with backup path", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should list valid backup files", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/backup"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - const backups = context.response.body as string[]; - expect(backups).toEqual( - expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]) - ); - - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with backup list", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Error Handling", () => { - it("should return proper error format", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/random_link", { - method: "GET", - headers: { "Content-Type": "application/json" }, - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(404); - - recordTestResult({ - name: "should return proper error format", - suite: - "API Configuration Endpoints - Error Handling of unkown routes", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should return proper error format", - suite: "API Configuration Endpoints - Error Handling", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "500 Error with structured error format", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.updateConfig.mockClear(); + }); + + describe("Core Configuration", () => { + it("should retrieve current config with hashed API key", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + fetching_interval: expect.any(Number), + keep_data_for: expect.any(Number), + }); + + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with valid config structure", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle config update with valid payload", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const requestBody = { + fetching_interval: 15, + keep_data_for: 30, + api_key: "new-valid-key", + }; + const req = new Request("http://localhost:3000/config/update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestBody), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + success: true, + message: expect.stringContaining("Updated"), + }); + + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Plugin Management", () => { + it("should list active plugins with metadata", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/plugins"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual( + [], + //expect.arrayContaining([ + // expect.objectContaining({ + // name: expect.any(String), + // version: expect.any(String), + // status: expect.any(String), + // }), + //]) + ); + + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with plugin list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Backup Management", () => { + it("should generate timestamped backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup", { + method: "POST", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const { message } = context.response.body as { message: string }; + expect(message).toMatch( + /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/, + ); + + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup path", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should list valid backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const backups = context.response.body as string[]; + expect(backups).toEqual( + expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]), + ); + + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Error Handling", () => { + it("should return proper error format", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/random_link", { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(404); + + recordTestResult({ + name: "should return proper error format", + suite: + "API Configuration Endpoints - Error Handling of unkown routes", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should return proper error format", + suite: "API Configuration Endpoints - Error Handling", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "500 Error with structured error format", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateJunitReport(); + generateJunitReport(); }); diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts index b8864e8a..962937a0 100644 --- a/src/tests/docker-manager.spec.ts +++ b/src/tests/docker-manager.spec.ts @@ -3,462 +3,462 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { dockerRoutes } from "~/routes/docker-manager"; import { - generateJunitReport, - recordTestResult, - testResults, + generateJunitReport, + recordTestResult, + testResults, } from "./junit-exporter"; import type { TestContext } from "./junit-exporter"; type DockerHost = { - id?: number; - name: string; - hostAddress: string; - secure: boolean; + id?: number; + name: string; + hostAddress: string; + secure: boolean; }; const mockDb = { - addDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - updateDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - getDockerHosts: mock(() => []), - deleteDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), + addDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + updateDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + getDockerHosts: mock(() => []), + deleteDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), }; mock.module("~/core/database", () => ({ - dbFunctions: mockDb, + dbFunctions: mockDb, })); mock.module("~/core/utils/logger", () => ({ - logger: { - debug: mock(), - info: mock(), - error: mock(), - }, + logger: { + debug: mock(), + info: mock(), + error: mock(), + }, })); const createApp = () => new Elysia().use(dockerRoutes).decorate({}); async function captureTestContext( - req: Request, - res: Response + req: Request, + res: Response, ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: unknown; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: unknown; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("Docker Configuration Endpoints", () => { - beforeEach(() => { - mockDb.addDockerHost.mockClear(); - mockDb.updateDockerHost.mockClear(); - mockDb.getDockerHosts.mockClear(); - mockDb.deleteDockerHost.mockClear(); - }); - - describe("POST /docker-config/add-host", () => { - it("should add a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host1", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Added docker host (${host.name})`, - }); - expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with success message", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when adding a docker host fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host2", - hostAddress: "invalid", - secure: true, - }; - - // Set mock implementation - mockDb.addDockerHost.mockImplementationOnce(() => { - throw new Error("DB error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error structure", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("POST /docker-config/update-host", () => { - it("should update a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 1, - name: "Host1-upd", - hostAddress: "127.0.0.1:2376", - secure: true, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Updated docker host (${host.id})`, - }); - expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when update fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 2, - name: "Host2", - hostAddress: "x", - secure: false, - }; - - mockDb.updateDockerHost.mockImplementationOnce(() => { - throw new Error("Update error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("GET /docker-config/hosts", () => { - it("should retrieve list of hosts", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const hosts: DockerHost[] = [ - { id: 1, name: "H1", hostAddress: "a", secure: false }, - ]; - - mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual(hosts); - - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with hosts array", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when retrieval fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - mockDb.getDockerHosts.mockImplementationOnce(() => { - throw new Error("Fetch error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("DELETE /docker-config/hosts/:id", () => { - it("should delete a host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 5; - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Deleted docker host (${id})`, - }); - expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); - - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with deletion confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when delete fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 6; - - mockDb.deleteDockerHost.mockImplementationOnce(() => { - throw new Error("Delete error"); - }); - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.addDockerHost.mockClear(); + mockDb.updateDockerHost.mockClear(); + mockDb.getDockerHosts.mockClear(); + mockDb.deleteDockerHost.mockClear(); + }); + + describe("POST /docker-config/add-host", () => { + it("should add a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host1", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Added docker host (${host.name})`, + }); + expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with success message", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when adding a docker host fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host2", + hostAddress: "invalid", + secure: true, + }; + + // Set mock implementation + mockDb.addDockerHost.mockImplementationOnce(() => { + throw new Error("DB error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error structure", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("POST /docker-config/update-host", () => { + it("should update a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 1, + name: "Host1-upd", + hostAddress: "127.0.0.1:2376", + secure: true, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Updated docker host (${host.id})`, + }); + expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when update fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 2, + name: "Host2", + hostAddress: "x", + secure: false, + }; + + mockDb.updateDockerHost.mockImplementationOnce(() => { + throw new Error("Update error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("GET /docker-config/hosts", () => { + it("should retrieve list of hosts", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const hosts: DockerHost[] = [ + { id: 1, name: "H1", hostAddress: "a", secure: false }, + ]; + + mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual(hosts); + + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with hosts array", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when retrieval fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + mockDb.getDockerHosts.mockImplementationOnce(() => { + throw new Error("Fetch error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("DELETE /docker-config/hosts/:id", () => { + it("should delete a host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 5; + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Deleted docker host (${id})`, + }); + expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); + + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with deletion confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when delete fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 6; + + mockDb.deleteDockerHost.mockImplementationOnce(() => { + throw new Error("Delete error"); + }); + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateJunitReport(); + generateJunitReport(); }); diff --git a/tsconfig.json b/tsconfig.json index fc07b9ba..dad4550b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -61,8 +61,8 @@ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ From 4b52cb2070e052c430d8f5da55030f5beb275550 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 14:20:34 +0000 Subject: [PATCH 307/369] Update dependency graphs --- dependency-graph.mmd | 5 +- dependency-graph.svg | 1057 +++++++++++++++++++++--------------------- 2 files changed, 519 insertions(+), 543 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 85674285..affe0471 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -17,7 +17,6 @@ X["live-logs.ts"] 1M["docker-websocket.ts"] 1O["logs.ts"] 1P["stacks.ts"] -1S["utils.ts"] end subgraph 9["core"] subgraph A["utils"] @@ -99,7 +98,6 @@ M["os"] 1-->X 1-->1O 1-->1P -1-->1S 1-->4 1-->5 7-->B @@ -223,6 +221,7 @@ Z-->S 1P-->1R 1P-->B 1P-->1J +1P-->4 1R-->W 1R-->F 1R-->B @@ -230,6 +229,4 @@ Z-->S 1R-->4 1R-->V 1R-->L -1S-->1C -1S-->1J diff --git a/dependency-graph.svg b/dependency-graph.svg index abb59e56..8e7e54b0 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,77 +4,77 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/utils - -utils + +utils cluster_src/middleware - -middleware + +middleware cluster_src/routes - -routes + +routes cluster_~ - -~ + +~ cluster_~/typings - -typings + +typings bun:sqlite - -bun:sqlite + +bun:sqlite @@ -91,8 +91,8 @@ events - -events + +events @@ -100,8 +100,8 @@ fs - -fs + +fs @@ -109,8 +109,8 @@ fs/promises - -promises + +promises @@ -118,8 +118,8 @@ os - -os + +os @@ -127,8 +127,8 @@ package.json - -package.json + +package.json @@ -136,8 +136,8 @@ path - -path + +path @@ -145,8 +145,8 @@ src/core/database/_dbState.ts - -_dbState.ts + +_dbState.ts @@ -154,902 +154,902 @@ src/core/database/backup.ts - -backup.ts + +backup.ts src/core/database/backup.ts->fs - - + + src/core/database/backup.ts->src/core/database/_dbState.ts - - + + src/core/database/database.ts - -database.ts + +database.ts src/core/database/backup.ts->src/core/database/database.ts - - + + src/core/database/helper.ts - -helper.ts + +helper.ts src/core/database/backup.ts->src/core/database/helper.ts - - - - + + + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + ~/typings/misc - -misc + +misc src/core/database/backup.ts->~/typings/misc - - + + src/core/database/database.ts->bun:sqlite - - + + src/core/database/database.ts->fs - - + + src/core/database/database.ts->fs/promises - - + + src/core/database/database.ts->os - - + + src/core/database/database.ts->path - - + + src/core/database/helper.ts->src/core/database/_dbState.ts - - + + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/utils/logger.ts->path - - + + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + src/core/database/index.ts - -index.ts + +index.ts src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + ~/typings/database - -database + +database src/core/utils/logger.ts->~/typings/database - - + + src/routes/live-logs.ts - -live-logs.ts + +live-logs.ts src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/core/database/config.ts - -config.ts + +config.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + ~/typings/docker - -docker + +docker src/core/database/hostStats.ts->~/typings/docker - - + + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + src/core/database/logs.ts->~/typings/database - - + + src/core/database/stacks.ts->src/core/database/database.ts - - + + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + src/core/database/stacks.ts->~/typings/database - - + + src/core/utils/helpers.ts - -helpers.ts + +helpers.ts src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - + + + + ~/typings/docker-compose - -docker-compose + +docker-compose src/core/database/stacks.ts->~/typings/docker-compose - - + + src/core/utils/helpers.ts->src/core/utils/logger.ts - - - - + + + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->~/typings/docker - - + + src/core/docker/monitor.ts - -monitor.ts + +monitor.ts src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + src/core/docker/monitor.ts->~/typings/docker - - + + src/core/docker/monitor.ts->src/core/database/index.ts - - + + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + src/core/plugins/plugin-manager.ts - -plugin-manager.ts + +plugin-manager.ts src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + src/core/plugins/plugin-manager.ts->events - - + + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/core/plugins/plugin-manager.ts->~/typings/docker - - + + ~/typings/plugin - -plugin + +plugin src/core/plugins/plugin-manager.ts->~/typings/plugin - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + src/core/docker/scheduler.ts->~/typings/database - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->~/typings/docker - - + + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + ~/typings/dockerode - -dockerode + +dockerode src/core/docker/store-host-stats.ts->~/typings/dockerode - - + + src/core/plugins/loader.ts - -loader.ts + +loader.ts src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + src/core/utils/change-me-checker.ts - -change-me-checker.ts + +change-me-checker.ts src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + src/core/utils/change-me-checker.ts->fs/promises - - + + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts src/core/stacks/controller.ts->fs/promises - - + + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts->src/core/database/index.ts - - + + src/core/stacks/controller.ts->~/typings/database - - + + src/core/stacks/controller.ts->src/core/utils/helpers.ts - - + + src/core/stacks/controller.ts->~/typings/docker-compose - - + + src/routes/live-stacks.ts - -live-stacks.ts + +live-stacks.ts src/core/stacks/controller.ts->src/routes/live-stacks.ts - - + + - + src/routes/live-stacks.ts->src/core/utils/logger.ts - - + + - + ~/typings/websocket - - -websocket + + +websocket - + src/routes/live-stacks.ts->~/typings/websocket - - + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->~/typings/database - - + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts src/core/utils/package-json.ts->package.json - - + + src/core/utils/response-handler.ts - -response-handler.ts + +response-handler.ts src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + ~/typings/elysiajs - -elysiajs + +elysiajs src/core/utils/response-handler.ts->~/typings/elysiajs - - + + src/core/utils/swagger-readme.ts - -swagger-readme.ts + +swagger-readme.ts @@ -1057,439 +1057,418 @@ src/index.ts - -index.ts + +index.ts - + src/index.ts->elysia-remote-dts - + src/index.ts->src/core/utils/logger.ts - - + + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->~/typings/database - - + + src/index.ts->src/core/docker/monitor.ts - - + + src/index.ts->src/core/docker/scheduler.ts - - + + src/index.ts->src/core/plugins/loader.ts - - + + src/index.ts->src/routes/live-stacks.ts - - + + src/index.ts->src/routes/live-logs.ts - - + + src/index.ts->src/core/utils/package-json.ts - - + + src/index.ts->src/core/utils/swagger-readme.ts - - + + src/middleware/auth.ts - -auth.ts + +auth.ts src/index.ts->src/middleware/auth.ts - - + + src/routes/api-config.ts - -api-config.ts + +api-config.ts src/index.ts->src/routes/api-config.ts - - + + src/routes/docker-manager.ts - -docker-manager.ts + +docker-manager.ts src/index.ts->src/routes/docker-manager.ts - - + + src/routes/docker-stats.ts - -docker-stats.ts + +docker-stats.ts src/index.ts->src/routes/docker-stats.ts - - + + src/routes/docker-websocket.ts - -docker-websocket.ts + +docker-websocket.ts src/index.ts->src/routes/docker-websocket.ts - - + + src/routes/logs.ts - -logs.ts + +logs.ts src/index.ts->src/routes/logs.ts - - + + src/routes/stacks.ts - -stacks.ts + +stacks.ts src/index.ts->src/routes/stacks.ts - - - - - -src/routes/utils.ts - - -utils.ts - - - - - -src/index.ts->src/routes/utils.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->~/typings/database - - + + - + src/middleware/auth.ts->~/typings/elysiajs - - + + - + src/routes/api-config.ts->fs - - + + - + src/routes/api-config.ts->src/core/database/backup.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->~/typings/database - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->~/typings/docker - - + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->~/typings/docker - - + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/helpers.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->~/typings/dockerode - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/index.ts - - + + + + + +src/routes/stacks.ts->~/typings/database + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - - - - -src/routes/utils.ts->src/core/utils/package-json.ts - - - - - -src/routes/utils.ts->src/core/utils/response-handler.ts - - + + From b826987124f0145dbb8a67b4424468e075752e0c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 16:26:33 +0200 Subject: [PATCH 308/369] CI/CD: Move lint into seperate workflow --- .github/workflows/ci.yml | 33 +------ .github/workflows/lint.yaml | 57 +++++++++++ dependency-graph.dot | 182 ------------------------------------ 3 files changed, 59 insertions(+), 213 deletions(-) create mode 100644 .github/workflows/lint.yaml delete mode 100644 dependency-graph.dot diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31877af5..8806b90b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,8 @@ on: branches: ["**"] jobs: - lint-test: - name: Lint and Test + unit-test: + name: Test runs-on: ubuntu-latest permissions: contents: write @@ -27,25 +27,6 @@ jobs: - name: Install dependencies run: bun install - - name: Knip check - if: ${{ github.event_name == 'pull_request' }} - uses: codex-/knip-reporter@v2 - - - name: Run linter - run: | - bun biome format --fix - bun biome lint --fix - bun biome check --fix - bun biome ci - - - name: Add linted files - run: git add src/ - - - name: Check for changes - id: check-changes - run: | - git diff --cached --quiet || echo "changes_detected=true" >> $GITHUB_OUTPUT - - name: Run unit tests run: | export PAD_NEW_LINES=false @@ -59,16 +40,6 @@ jobs: with: report_paths: "reports/junit/*.xml" - - name: Commit and push lint changes - if: | - steps.check-changes.outputs.changes_detected == 'true' && - github.event_name == 'push' - run: | - git config --global user.name "GitHub Actions" - git config --global user.email "actions@github.com" - git commit -m "CQL: Apply lint fixes [skip ci]" - git push - build-scan: name: Build and Security Scan runs-on: ubuntu-latest diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 00000000..6a253b17 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,57 @@ +name: Lint + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + lint-test: + name: Lint + runs-on: ubuntu-latest + permissions: + contents: write + checks: write + security-events: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Knip check + if: ${{ github.event_name == 'pull_request' }} + uses: codex-/knip-reporter@v2 + + - name: Run linter + run: | + bun biome format --fix + bun biome lint --fix + bun biome check --fix + bun biome ci + + - name: Add linted files + run: git add src/ + + - name: Check for changes + id: check-changes + run: | + git diff --cached --quiet || echo "changes_detected=true" >> $GITHUB_OUTPUT + + - name: Commit and push lint changes + if: | + steps.check-changes.outputs.changes_detected == 'true' && + github.event_name == 'push' + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + git commit -m "CQL: Apply lint fixes [skip ci]" + git push diff --git a/dependency-graph.dot b/dependency-graph.dot deleted file mode 100644 index 7c5bbef1..00000000 --- a/dependency-graph.dot +++ /dev/null @@ -1,182 +0,0 @@ -strict digraph "dependency-cruiser output"{ - rankdir="LR" splines="true" overlap="false" nodesep="0.16" ranksep="0.18" fontname="Helvetica-bold" fontsize="9" style="rounded,bold,filled" fillcolor="#ffffff" compound="true" - node [shape="box" style="rounded, filled" height="0.2" color="black" fillcolor="#ffffcc" fontcolor="black" fontname="Helvetica" fontsize="9"] - edge [arrowhead="normal" arrowsize="0.6" penwidth="2.0" color="#00000033" fontname="Helvetica" fontsize="9"] - - "bun" [label= tooltip="bun" ] - "bun:sqlite" [label= tooltip="bun:sqlite" ] - "events" [label= tooltip="events" URL="https://nodejs.org/api/events.html" color="grey" fontcolor="grey"] - "fs" [label= tooltip="fs" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] - subgraph "cluster_fs" {label="fs" "fs/promises" [label= tooltip="promises" URL="https://nodejs.org/api/fs.html" color="grey" fontcolor="grey"] } - "package.json" [label= tooltip="package.json" URL="package.json" fillcolor="#ffee44"] - "path" [label= tooltip="path" URL="https://nodejs.org/api/path.html" color="grey" fontcolor="grey"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/config.ts" [label= tooltip="config.ts" URL="src/core/database/config.ts" fillcolor="#ddfeff"] } } } - "src/core/database/config.ts" -> "src/core/database/database.ts" - "src/core/database/config.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/containerStats.ts" [label= tooltip="containerStats.ts" URL="src/core/database/containerStats.ts" fillcolor="#ddfeff"] } } } - "src/core/database/containerStats.ts" -> "src/core/database/database.ts" - "src/core/database/containerStats.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/database.ts" [label= tooltip="database.ts" URL="src/core/database/database.ts" fillcolor="#ddfeff"] } } } - "src/core/database/database.ts" -> "bun:sqlite" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/dockerHosts.ts" [label= tooltip="dockerHosts.ts" URL="src/core/database/dockerHosts.ts" fillcolor="#ddfeff"] } } } - "src/core/database/dockerHosts.ts" -> "src/core/database/database.ts" - "src/core/database/dockerHosts.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - "src/core/database/dockerHosts.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/helper.ts" [label= tooltip="helper.ts" URL="src/core/database/helper.ts" fillcolor="#ddfeff"] } } } - "src/core/database/helper.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/hostStats.ts" [label= tooltip="hostStats.ts" URL="src/core/database/hostStats.ts" fillcolor="#ddfeff"] } } } - "src/core/database/hostStats.ts" -> "src/core/database/database.ts" - "src/core/database/hostStats.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - "src/core/database/hostStats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/index.ts" [label= tooltip="index.ts" URL="src/core/database/index.ts" fillcolor="#ddfeff"] } } } - "src/core/database/index.ts" -> "src/core/database/config.ts" [arrowhead="normalnoneodot"] - "src/core/database/index.ts" -> "src/core/database/containerStats.ts" [arrowhead="normalnoneodot"] - "src/core/database/index.ts" -> "src/core/database/database.ts" - "src/core/database/index.ts" -> "src/core/database/dockerHosts.ts" [arrowhead="normalnoneodot"] - "src/core/database/index.ts" -> "src/core/database/hostStats.ts" [arrowhead="normalnoneodot"] - "src/core/database/index.ts" -> "src/core/database/logs.ts" [arrowhead="normalnoneodot"] - "src/core/database/index.ts" -> "src/core/database/stacks.ts" [arrowhead="normalnoneodot"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/logs.ts" [label= tooltip="logs.ts" URL="src/core/database/logs.ts" fillcolor="#ddfeff"] } } } - "src/core/database/logs.ts" -> "src/core/database/database.ts" - "src/core/database/logs.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - "src/core/database/logs.ts" -> "src/typings/websocket.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/database" {label="database" "src/core/database/stacks.ts" [label= tooltip="stacks.ts" URL="src/core/database/stacks.ts" fillcolor="#ddfeff"] } } } - "src/core/database/stacks.ts" -> "src/core/database/database.ts" - "src/core/database/stacks.ts" -> "src/core/database/helper.ts" [arrowhead="normalnoneodot"] - "src/core/database/stacks.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/database/stacks.ts" -> "src/typings/docker-compose.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/client.ts" [label= tooltip="client.ts" URL="src/core/docker/client.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/client.ts" -> "src/core/utils/logger.ts" - "src/core/docker/client.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/monitor.ts" [label= tooltip="monitor.ts" URL="src/core/docker/monitor.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/monitor.ts" -> "src/core/plugins/plugin-manager.ts" - "src/core/docker/monitor.ts" -> "src/core/database/index.ts" - "src/core/docker/monitor.ts" -> "src/core/docker/client.ts" - "src/core/docker/monitor.ts" -> "src/core/utils/logger.ts" - "src/core/docker/monitor.ts" -> "src/typings/docker.ts" - "src/core/docker/monitor.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/docker/monitor.ts" -> "bun" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/scheduler.ts" [label= tooltip="scheduler.ts" URL="src/core/docker/scheduler.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/scheduler.ts" -> "src/core/database/index.ts" - "src/core/docker/scheduler.ts" -> "src/core/docker/store-host-stats.ts" - "src/core/docker/scheduler.ts" -> "src/core/docker/store-container-stats.ts" - "src/core/docker/scheduler.ts" -> "src/core/utils/logger.ts" - "src/core/docker/scheduler.ts" -> "src/typings/database.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-container-stats.ts" [label= tooltip="store-container-stats.ts" URL="src/core/docker/store-container-stats.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/store-container-stats.ts" -> "src/core/utils/logger.ts" - "src/core/docker/store-container-stats.ts" -> "src/core/database/index.ts" - "src/core/docker/store-container-stats.ts" -> "src/core/docker/client.ts" - "src/core/docker/store-container-stats.ts" -> "src/core/utils/calculations.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/docker" {label="docker" "src/core/docker/store-host-stats.ts" [label= tooltip="store-host-stats.ts" URL="src/core/docker/store-host-stats.ts" fillcolor="#ddfeff"] } } } - "src/core/docker/store-host-stats.ts" -> "src/core/database/index.ts" - "src/core/docker/store-host-stats.ts" -> "src/core/docker/client.ts" - "src/core/docker/store-host-stats.ts" -> "src/core/utils/logger.ts" - "src/core/docker/store-host-stats.ts" -> "src/typings/docker.ts" - "src/core/docker/store-host-stats.ts" -> "src/typings/dockerode.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/loader.ts" [label= tooltip="loader.ts" URL="src/core/plugins/loader.ts" fillcolor="#ddfeff"] } } } - "src/core/plugins/loader.ts" -> "src/core/utils/change-me-checker.ts" - "src/core/plugins/loader.ts" -> "src/core/utils/logger.ts" - "src/core/plugins/loader.ts" -> "src/core/plugins/plugin-manager.ts" - "src/core/plugins/loader.ts" -> "fs" [style="dashed" penwidth="1.0"] - "src/core/plugins/loader.ts" -> "path" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/plugins" {label="plugins" "src/core/plugins/plugin-manager.ts" [label= tooltip="plugin-manager.ts" URL="src/core/plugins/plugin-manager.ts" fillcolor="#ddfeff"] } } } - "src/core/plugins/plugin-manager.ts" -> "src/core/utils/logger.ts" - "src/core/plugins/plugin-manager.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/plugins/plugin-manager.ts" -> "src/typings/plugin.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/plugins/plugin-manager.ts" -> "events" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/stacks" {label="stacks" "src/core/stacks/controller.ts" [label= tooltip="controller.ts" URL="src/core/stacks/controller.ts" fillcolor="#ddfeff"] } } } - "src/core/stacks/controller.ts" -> "src/core/database/index.ts" - "src/core/stacks/controller.ts" -> "src/core/utils/logger.ts" - "src/core/stacks/controller.ts" -> "src/typings/database.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/stacks/controller.ts" -> "src/typings/docker-compose.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/stacks/controller.ts" -> "bun" - "src/core/stacks/controller.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/calculations.ts" [label= tooltip="calculations.ts" URL="src/core/utils/calculations.ts" fillcolor="#ddfeff"] } } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/change-me-checker.ts" [label= tooltip="change-me-checker.ts" URL="src/core/utils/change-me-checker.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/change-me-checker.ts" -> "src/core/utils/logger.ts" - "src/core/utils/change-me-checker.ts" -> "fs/promises" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/logger.ts" [label= tooltip="logger.ts" URL="src/core/utils/logger.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/logger.ts" -> "src/core/database/index.ts" [arrowhead="normalnoneodot"] - "src/core/utils/logger.ts" -> "src/routes/live-logs.ts" [arrowhead="normalnoneodot"] - "src/core/utils/logger.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] - "src/core/utils/logger.ts" -> "path" [style="dashed" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/package-json.ts" [label= tooltip="package-json.ts" URL="src/core/utils/package-json.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/package-json.ts" -> "package.json" - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/response-handler.ts" [label= tooltip="response-handler.ts" URL="src/core/utils/response-handler.ts" fillcolor="#ddfeff"] } } } - "src/core/utils/response-handler.ts" -> "src/core/utils/logger.ts" - "src/core/utils/response-handler.ts" -> "src/typings/elysiajs.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/core" {label="core" subgraph "cluster_src/core/utils" {label="utils" "src/core/utils/swagger-readme.ts" [label= tooltip="swagger-readme.ts" URL="src/core/utils/swagger-readme.ts" fillcolor="#ddfeff"] } } } - subgraph "cluster_src" {label="src" "src/index.ts" [label= tooltip="index.ts" URL="src/index.ts" fillcolor="#ddfeff"] } - "src/index.ts" -> "src/core/docker/monitor.ts" - "src/index.ts" -> "src/core/utils/swagger-readme.ts" - "src/index.ts" -> "src/middleware/auth.ts" - "src/index.ts" -> "src/routes/live-logs.ts" - "src/index.ts" -> "src/routes/stacks.ts" - "src/index.ts" -> "src/routes/utils.ts" - "src/index.ts" -> "src/typings/database.ts" - "src/index.ts" -> "src/core/database/index.ts" - "src/index.ts" -> "src/core/docker/scheduler.ts" - "src/index.ts" -> "src/core/plugins/loader.ts" - "src/index.ts" -> "src/core/utils/logger.ts" - "src/index.ts" -> "src/routes/api-config.ts" - "src/index.ts" -> "src/routes/docker-manager.ts" - "src/index.ts" -> "src/routes/docker-stats.ts" - "src/index.ts" -> "src/routes/docker-websocket.ts" - "src/index.ts" -> "src/routes/logs.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/middleware" {label="middleware" "src/middleware/auth.ts" [label= tooltip="auth.ts" URL="src/middleware/auth.ts" fillcolor="#ddfeff"] } } - "src/middleware/auth.ts" -> "src/core/database/index.ts" - "src/middleware/auth.ts" -> "src/core/utils/logger.ts" - "src/middleware/auth.ts" -> "src/typings/database.ts" - "src/middleware/auth.ts" -> "src/typings/elysiajs.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/api-config.ts" [label= tooltip="api-config.ts" URL="src/routes/api-config.ts" fillcolor="#ddfeff"] } } - "src/routes/api-config.ts" -> "src/core/database/index.ts" - "src/routes/api-config.ts" -> "src/core/plugins/plugin-manager.ts" - "src/routes/api-config.ts" -> "src/core/utils/logger.ts" - "src/routes/api-config.ts" -> "src/core/utils/package-json.ts" - "src/routes/api-config.ts" -> "src/core/utils/response-handler.ts" - "src/routes/api-config.ts" -> "src/middleware/auth.ts" - "src/routes/api-config.ts" -> "src/typings/database.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-manager.ts" [label= tooltip="docker-manager.ts" URL="src/routes/docker-manager.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-manager.ts" -> "src/core/database/index.ts" - "src/routes/docker-manager.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-manager.ts" -> "src/core/utils/response-handler.ts" - "src/routes/docker-manager.ts" -> "src/typings/docker.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-stats.ts" [label= tooltip="docker-stats.ts" URL="src/routes/docker-stats.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-stats.ts" -> "src/core/database/index.ts" - "src/routes/docker-stats.ts" -> "src/core/docker/client.ts" - "src/routes/docker-stats.ts" -> "src/core/utils/calculations.ts" - "src/routes/docker-stats.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-stats.ts" -> "src/core/utils/response-handler.ts" - "src/routes/docker-stats.ts" -> "src/typings/docker.ts" [arrowhead="onormal" penwidth="1.0"] - "src/routes/docker-stats.ts" -> "src/typings/dockerode.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/docker-websocket.ts" [label= tooltip="docker-websocket.ts" URL="src/routes/docker-websocket.ts" fillcolor="#ddfeff"] } } - "src/routes/docker-websocket.ts" -> "src/core/database/index.ts" - "src/routes/docker-websocket.ts" -> "src/core/docker/client.ts" - "src/routes/docker-websocket.ts" -> "src/core/utils/calculations.ts" - "src/routes/docker-websocket.ts" -> "src/core/utils/logger.ts" - "src/routes/docker-websocket.ts" -> "src/core/utils/response-handler.ts" - "src/routes/docker-websocket.ts" -> "stream" [style="dashed" penwidth="1.0" arrowhead="onormal"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/live-logs.ts" [label= tooltip="live-logs.ts" URL="src/routes/live-logs.ts" fillcolor="#ddfeff"] } } - "src/routes/live-logs.ts" -> "src/core/utils/logger.ts" [arrowhead="normalnoneodot"] - "src/routes/live-logs.ts" -> "src/typings/websocket.ts" [arrowhead="onormal" penwidth="1.0"] - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/logs.ts" [label= tooltip="logs.ts" URL="src/routes/logs.ts" fillcolor="#ddfeff"] } } - "src/routes/logs.ts" -> "src/core/database/index.ts" - "src/routes/logs.ts" -> "src/core/utils/logger.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/stacks.ts" [label= tooltip="stacks.ts" URL="src/routes/stacks.ts" fillcolor="#ddfeff"] } } - "src/routes/stacks.ts" -> "src/core/database/index.ts" - "src/routes/stacks.ts" -> "src/core/stacks/controller.ts" - "src/routes/stacks.ts" -> "src/core/utils/logger.ts" - "src/routes/stacks.ts" -> "src/core/utils/response-handler.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/routes" {label="routes" "src/routes/utils.ts" [label= tooltip="utils.ts" URL="src/routes/utils.ts" fillcolor="#ddfeff"] } } - "src/routes/utils.ts" -> "src/core/utils/package-json.ts" - "src/routes/utils.ts" -> "src/core/utils/response-handler.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/database.ts" [label= tooltip="database.ts" URL="src/typings/database.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker-compose.ts" [label= tooltip="docker-compose.ts" URL="src/typings/docker-compose.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/docker.ts" [label= tooltip="docker.ts" URL="src/typings/docker.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/dockerode.ts" [label= tooltip="dockerode.ts" URL="src/typings/dockerode.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/elysiajs.ts" [label= tooltip="elysiajs.ts" URL="src/typings/elysiajs.ts" fillcolor="#ddfeff"] } } - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/plugin.ts" [label= tooltip="plugin.ts" URL="src/typings/plugin.ts" fillcolor="#ddfeff"] } } - "src/typings/plugin.ts" -> "src/typings/docker.ts" - subgraph "cluster_src" {label="src" subgraph "cluster_src/typings" {label="typings" "src/typings/websocket.ts" [label= tooltip="websocket.ts" URL="src/typings/websocket.ts" fillcolor="#ddfeff"] } } - "stream" [label= tooltip="stream" URL="https://nodejs.org/api/stream.html" color="grey" fontcolor="grey"] -} From 7a3253d981de9fb8f45d4684eb4f4843e11700ad Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 14 May 2025 14:26:56 +0000 Subject: [PATCH 309/369] CQL: Apply lint fixes [skip ci] --- src/core/stacks/controller.ts | 682 +++++++++++++++++----------------- src/routes/live-stacks.ts | 34 +- 2 files changed, 358 insertions(+), 358 deletions(-) diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 95a6480a..1f506bf8 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -9,393 +9,393 @@ import type { ComposeSpec, Stack } from "~/typings/docker-compose"; import { findObjectByKey } from "../utils/helpers"; const wrapProgressCallback = (progressCallback?: (log: string) => void) => { - return progressCallback - ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { - const log = chunk.toString(); - progressCallback(log); - } - : undefined; + return progressCallback + ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; }; async function getStackName(stack_id: number): Promise { - logger.debug(`Fetching stack name for id ${stack_id}`); - const stacks = dbFunctions.getStacks(); - const stack = findObjectByKey(stacks, "id", stack_id); - if (!stack) { - throw new Error(`Stack with id ${stack_id} not found`); - } - return stack.name; + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = findObjectByKey(stacks, "id", stack_id); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; } async function runStackCommand( - stack_id: number, - command: ( - cwd: string, - progressCallback?: (log: string) => void - ) => Promise, - action: string + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void, + ) => Promise, + action: string, ): Promise { - try { - logger.debug( - `Starting runStackCommand for stack_id=${stack_id}, action="${action}"` - ); - - const stackName = await getStackName(stack_id); - logger.debug( - `Retrieved stack name "${stackName}" for stack_id=${stack_id}` - ); - - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); - - const progressCallback = (log: string) => { - const message = log.trim(); - logger.debug( - `Progress for stack_id=${stack_id}, action="${action}": ${message}` - ); - - // ERROR HANDLING FOR COMPOSE ACTIONS - if (message.includes("Error response from daemon")) { - logger.error( - `Error response from daemon: ${ - message.split("Error response from daemon:")[1] - }` - ); - } - - postToClient({ - type: "stack-progress", - data: { - stack_id, - action, - message, - timestamp: new Date().toISOString(), - }, - }); - }; - - logger.debug( - `Executing command for stack_id=${stack_id}, action="${action}"` - ); - const result = await command(stackPath, progressCallback); - logger.debug( - `Successfully completed command for stack_id=${stack_id}, action="${action}"` - ); - - return result; - } catch (error) { - logger.debug( - `Error occurred for stack_id=${stack_id}, action="${action}": ${String( - error - )}` - ); - postToClient({ - type: "stack-error", - data: { - stack_id, - action, - message: String(error), - timestamp: new Date().toISOString(), - }, - }); - throw new Error( - `Error while ${action} stack "${stack_id}": ${String(error)}` - ); - } + try { + logger.debug( + `Starting runStackCommand for stack_id=${stack_id}, action="${action}"`, + ); + + const stackName = await getStackName(stack_id); + logger.debug( + `Retrieved stack name "${stackName}" for stack_id=${stack_id}`, + ); + + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); + + const progressCallback = (log: string) => { + const message = log.trim(); + logger.debug( + `Progress for stack_id=${stack_id}, action="${action}": ${message}`, + ); + + // ERROR HANDLING FOR COMPOSE ACTIONS + if (message.includes("Error response from daemon")) { + logger.error( + `Error response from daemon: ${ + message.split("Error response from daemon:")[1] + }`, + ); + } + + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message, + timestamp: new Date().toISOString(), + }, + }); + }; + + logger.debug( + `Executing command for stack_id=${stack_id}, action="${action}"`, + ); + const result = await command(stackPath, progressCallback); + logger.debug( + `Successfully completed command for stack_id=${stack_id}, action="${action}"`, + ); + + return result; + } catch (error) { + logger.debug( + `Error occurred for stack_id=${stack_id}, action="${action}": ${String( + error, + )}`, + ); + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: String(error), + timestamp: new Date().toISOString(), + }, + }); + throw new Error( + `Error while ${action} stack "${stack_id}": ${String(error)}`, + ); + } } async function getStackPath(stack: Stack): Promise { - const stackName = stack.name.trim().replace(/\s+/g, "_"); - const stackId = stack.id; + const stackName = stack.name.trim().replace(/\s+/g, "_"); + const stackId = stack.id; - if (!stackId) { - logger.error("Stack could not be parsed"); - throw new Error("Stack could not be parsed"); - } + if (!stackId) { + logger.error("Stack could not be parsed"); + throw new Error("Stack could not be parsed"); + } - return `stacks/${stackId}-${stackName}`; + return `stacks/${stackId}-${stackName}`; } async function createStackYAML(compose_spec: Stack): Promise { - const yaml = YAML.stringify(compose_spec.compose_spec); - const stackPath = await getStackPath(compose_spec); - await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { - createPath: true, - }); + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); } export async function deployStack(stack_config: stacks_config): Promise { - try { - logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); - - if (!stack_config.name) { - throw new Error("Stack name needed"); - } - - const jsonStringStack = { - ...stack_config, - compose_spec: JSON.stringify(stack_config.compose_spec), - }; - - const stackId = dbFunctions.addStack(jsonStringStack); - - if (!stackId) { - throw new Error("Failed to add stack to database"); - } - - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "pending", - message: "Creating stack configuration", - }, - }); - - const stackYaml: Stack = { - id: stackId, - name: stack_config.name, - source: stack_config.source, - version: stack_config.version, - compose_spec: stack_config.compose_spec as unknown as ComposeSpec, // Weird stuff i am doing here... smh - }; - - await createStackYAML(stackYaml); - - await runStackCommand( - stackId, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "deploying" - ); - - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "deployed", - message: "Stack deployed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id: 0, - action: "deploying", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); + + if (!stack_config.name) { + throw new Error("Stack name needed"); + } + + const jsonStringStack = { + ...stack_config, + compose_spec: JSON.stringify(stack_config.compose_spec), + }; + + const stackId = dbFunctions.addStack(jsonStringStack); + + if (!stackId) { + throw new Error("Failed to add stack to database"); + } + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }); + + const stackYaml: Stack = { + id: stackId, + name: stack_config.name, + source: stack_config.source, + version: stack_config.version, + compose_spec: stack_config.compose_spec as unknown as ComposeSpec, // Weird stuff i am doing here... smh + }; + + await createStackYAML(stackYaml); + + await runStackCommand( + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying", + ); + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id: 0, + action: "deploying", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } export async function stopStack(stack_id: number): Promise { - // Note the await to discard the result (convert to void) - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.downAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "stopping" - ); + // Note the await to discard the result (convert to void) + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.downAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping", + ); } export async function startStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "starting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting", + ); } export async function pullStackImages(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.pullAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "pulling-images" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images", + ); } export async function restartStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.restartAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "restarting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting", + ); } export async function getStackStatus( - stack_id: number - //biome-ignore lint/suspicious/noExplicitAny: + stack_id: number, + //biome-ignore lint/suspicious/noExplicitAny: ): Promise> { - const status = await runStackCommand( - stack_id, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - //biome-ignore lint/suspicious/noExplicitAny: - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check" - ); - return status; + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check", + ); + return status; } export async function removeStack(stack_id: number): Promise { - try { - const _ = dbFunctions.deleteStack(stack_id); - - await runStackCommand( - stack_id, - async (cwd, progressCallback) => { - await DockerCompose.down({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }); - }, - "removing" - ); - - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - - try { - await rm(stackPath, { recursive: true }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } - - postToClient({ - type: "stack-removed", - data: { - stack_id, - message: "Stack removed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + try { + const _ = dbFunctions.deleteStack(stack_id); + + await runStackCommand( + stack_id, + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }); + }, + "removing", + ); + + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + + try { + await rm(stackPath, { recursive: true }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } + + postToClient({ + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } interface DockerServiceStatus { - status: string; - ports: string[]; + status: string; + ports: string[]; } interface StackStatus { - services: Record; - healthy: number; - unhealthy: number; - total: number; + services: Record; + healthy: number; + unhealthy: number; + total: number; } type StacksStatus = Record; export async function getAllStacksStatus(): Promise { - try { - const stacks = dbFunctions.getStacks(); - - const statusResults = await Promise.all( - stacks.map(async (stack) => { - const status = await runStackCommand( - stack.id as number, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - const services = rawStatus.data.services.reduce( - (acc: Record, service) => { - acc[service.name] = { - status: service.state, - ports: service.ports.map( - (port) => `${port.mapped?.address}:${port.mapped?.port}` - ), - }; - return acc; - }, - {} - ); - - const statusValues = Object.values(services); - return { - services, - healthy: statusValues.filter( - (s) => s.status === "running" || s.status.includes("Up") - ).length, - unhealthy: statusValues.filter( - (s) => s.status !== "running" && !s.status.includes("Up") - ).length, - total: statusValues.length, - }; - }, - "status-check" - ); - return { stackId: stack.id, status }; - }) - ); - - return statusResults.reduce((acc, { stackId, status }) => { - acc[String(stackId)] = status; - return acc; - }, {} as StacksStatus); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + try { + const stacks = dbFunctions.getStacks(); + + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.id as number, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + const services = rawStatus.data.services.reduce( + (acc: Record, service) => { + acc[service.name] = { + status: service.state, + ports: service.ports.map( + (port) => `${port.mapped?.address}:${port.mapped?.port}`, + ), + }; + return acc; + }, + {}, + ); + + const statusValues = Object.values(services); + return { + services, + healthy: statusValues.filter( + (s) => s.status === "running" || s.status.includes("Up"), + ).length, + unhealthy: statusValues.filter( + (s) => s.status !== "running" && !s.status.includes("Up"), + ).length, + total: statusValues.length, + }; + }, + "status-check", + ); + return { stackId: stack.id, status }; + }), + ); + + return statusResults.reduce((acc, { stackId, status }) => { + acc[String(stackId)] = status; + return acc; + }, {} as StacksStatus); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } diff --git a/src/routes/live-stacks.ts b/src/routes/live-stacks.ts index 2b5e8e3a..77aa2857 100644 --- a/src/routes/live-stacks.ts +++ b/src/routes/live-stacks.ts @@ -7,24 +7,24 @@ import type { stackSocketMessage } from "~/typings/websocket"; const activeConnections = new Set>(); export const liveStacks = new Elysia().ws("/stacks", { - open(ws) { - activeConnections.add(ws); - ws.send({ message: "Connection established" }); - logger.info(`New Stacks WebSocket established (${ws.id})`); - }, - close(ws) { - logger.info(`Stacks WebSocket closed (${ws.id})`); - activeConnections.delete(ws); - }, + open(ws) { + activeConnections.add(ws); + ws.send({ message: "Connection established" }); + logger.info(`New Stacks WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Stacks WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, }); export function postToClient(data: stackSocketMessage) { - for (const ws of activeConnections) { - try { - ws.send(JSON.stringify(data)); - } catch (error) { - activeConnections.delete(ws); - logger.error("Failed to send to WebSocket:", error); - } - } + for (const ws of activeConnections) { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + } } From 567dc2e5d0c2e577d9de0d4d10d1890292053fdb Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 16:55:39 +0200 Subject: [PATCH 310/369] CI/CD: Fix readability --- .github/workflows/ci.yml | 2 -- .github/workflows/lint.yaml | 2 -- 2 files changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8806b90b..a3fcd84b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,7 @@ name: Continuous Integration on: push: - branches: ["**"] pull_request: - branches: ["**"] jobs: unit-test: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 6a253b17..ca611f37 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -2,9 +2,7 @@ name: Lint on: push: - branches: ["**"] pull_request: - branches: ["**"] jobs: lint-test: From 86fb8955340222322fc4980e9140729e1fb21710 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 16:59:03 +0200 Subject: [PATCH 311/369] CI/CD: Fix readability and non existent dependant test --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3fcd84b..e8da5cec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ jobs: contents: write checks: write security-events: write + steps: - uses: actions/checkout@v4 with: @@ -41,11 +42,12 @@ jobs: build-scan: name: Build and Security Scan runs-on: ubuntu-latest - needs: lint-test + needs: unit-test permissions: contents: read checks: write security-events: write + steps: - uses: actions/checkout@v4 From f722a60542d4250a53d3b99bf2cf8e0dd1a00dfa Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:04:56 +0200 Subject: [PATCH 312/369] CI/CD: Fix gitignore --- .github/workflows/cd.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/dependency-graph.yml | 2 +- .github/workflows/lint.yaml | 2 +- .gitignore | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 1b60b66a..e7061106 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,4 +1,4 @@ -name: Continuous Delivery +name: "Continuous Delivery" on: release: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8da5cec..8c156622 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Continuous Integration +name: "Continuous Integration" on: push: diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 4fad4005..9e178ce7 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -1,4 +1,4 @@ -name: Generate Dependency Graph +name: "Generate Dependency Graph" on: push: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index ca611f37..5fe920a7 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -1,4 +1,4 @@ -name: Lint +name: "Lint" on: push: diff --git a/.gitignore b/.gitignore index 78bc2da9..9b149483 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,6 @@ build data *.xml -dependency-* +dependency-*.{mmd,dot,svg} Knip-Report.md -reports/** \ No newline at end of file +reports/** From 1854513984d69b20f39021e0633cc0929dd0e8b5 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:07:20 +0200 Subject: [PATCH 313/369] Fix: Just commit the data folder or smth to fix this weird error... --- .gitignore | 1 - data/.gitignore | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 data/.gitignore diff --git a/.gitignore b/.gitignore index 9b149483..e34b9b1e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ /node_modules .test build -data *.xml dependency-*.{mmd,dot,svg} Knip-Report.md diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 00000000..aed31992 --- /dev/null +++ b/data/.gitignore @@ -0,0 +1 @@ +./dockstatapi* From 5198159937023b7fad8f6bfc3a7319da852efbeb Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:10:40 +0200 Subject: [PATCH 314/369] CI/CD: Debug junit reporter --- .github/workflows/ci.yml | 4 ++++ package.json | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c156622..0c0b0522 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,10 @@ jobs: bun test bun clean + - name: Log unit test files + run: | + ls -lah reports/junit + - name: Publish Test Report if: always() uses: mikepenz/action-junit-report@v5 diff --git a/package.json b/package.json index 15ffaf20..c010efbe 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,7 @@ "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi* && cmd /c del /Q reports/junit/*.xml && echo 'success'", "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi* && rm -f reports/junit/*.xml && echo 'success'", "knip": "knip", - "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src", - "test": "bun test src/tests/**/*.test.ts" + "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src" }, "dependencies": { "@elysiajs/server-timing": "^1.3.0", From b0edecd009dbf84db319ba63ab78c3c5dadb35c6 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:15:00 +0200 Subject: [PATCH 315/369] CI/CD: Maybe do not delete the files before using them... --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c0b0522..5b40a3e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: jobs: unit-test: - name: Test + name: Unit Testing runs-on: ubuntu-latest permissions: contents: write @@ -31,7 +31,6 @@ jobs: export PAD_NEW_LINES=false docker compose -f docker/docker-compose.dev.yaml up -d bun test - bun clean - name: Log unit test files run: | From 935304bf2593519b5e699effdf5d522814a74fa4 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:16:31 +0200 Subject: [PATCH 316/369] CI/CD: Always publish test results --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b40a3e3..f302b58e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,7 @@ jobs: uses: mikepenz/action-junit-report@v5 with: report_paths: "reports/junit/*.xml" + include_passed: true build-scan: name: Build and Security Scan From 64c0dc80a5a4796032a5ae61d98540d574f54562 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:26:14 +0200 Subject: [PATCH 317/369] CI/CD: New test results --- .github/workflows/ci.yml | 13 +- package.json | 4 +- src/tests/api-config.spec.ts | 638 +++++++++++------------ src/tests/docker-manager.spec.ts | 870 +++++++++++++++---------------- src/tests/junit-exporter.ts | 145 ------ src/tests/markdown-exporter.ts | 141 +++++ 6 files changed, 904 insertions(+), 907 deletions(-) delete mode 100644 src/tests/junit-exporter.ts create mode 100644 src/tests/markdown-exporter.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f302b58e..5e687e49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,15 +34,16 @@ jobs: - name: Log unit test files run: | - ls -lah reports/junit + ls -lah reports/markdown - name: Publish Test Report if: always() - uses: mikepenz/action-junit-report@v5 - with: - report_paths: "reports/junit/*.xml" - include_passed: true - + run: | + SUMMARY="" + for element in $(ls reports/markdown); do + SUMMARY="$(echo -e "${SUMMARY}\n$(cat "${element}")")" + done + cho "$SUMMARY" >> $GITHUB_STEP_SUMMARY build-scan: name: Build and Security Scan runs-on: ubuntu-latest diff --git a/package.json b/package.json index c010efbe..c4e322e6 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "build:prod": "NODE_ENV=production bun build --no-native --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts", "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", "clean": "bun run clean:win || bun run clean:lin", - "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi* && cmd /c del /Q reports/junit/*.xml && echo 'success'", - "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi* && rm -f reports/junit/*.xml && echo 'success'", + "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi* && cmd /c del /Q reports/markdown/*.md && echo 'success'", + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi* && rm -f reports/markdown/*.md && echo 'success'", "knip": "knip", "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src" }, diff --git a/src/tests/api-config.spec.ts b/src/tests/api-config.spec.ts index d1f9d098..eb62cc83 100644 --- a/src/tests/api-config.spec.ts +++ b/src/tests/api-config.spec.ts @@ -2,343 +2,343 @@ import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; import { Elysia } from "elysia"; import { logger } from "~/core/utils/logger"; import { apiConfigRoutes } from "~/routes/api-config"; -import { generateJunitReport, recordTestResult } from "./junit-exporter"; -import type { TestContext } from "./junit-exporter"; +import { generateMarkdownReport, recordTestResult } from "./markdown-exporter"; +import type { TestContext } from "./markdown-exporter"; const mockDb = { - updateConfig: mock(() => ({})), - backupDatabase: mock( - () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak`, - ), - restoreDatabase: mock(), - findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), + updateConfig: mock(() => ({})), + backupDatabase: mock( + () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak` + ), + restoreDatabase: mock(), + findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), }; mock.module("node:fs", () => ({ - existsSync: mock((path) => path.includes("dockstatapi")), - readdirSync: mock(() => [ - "dockstatapi-2025-05-06.db.bak", - "dockstatapi.db", - "dockstatapi.db-shm", - ]), - unlinkSync: mock(), + existsSync: mock((path) => path.includes("dockstatapi")), + readdirSync: mock(() => [ + "dockstatapi-2025-05-06.db.bak", + "dockstatapi.db", + "dockstatapi.db-shm", + ]), + unlinkSync: mock(), })); const mockPlugins = [ - { - name: "docker-monitor", - version: "1.2.0", - status: "active", - }, + { + name: "docker-monitor", + version: "1.2.0", + status: "active", + }, ]; const createTestApp = () => - new Elysia().use(apiConfigRoutes).decorate({ - dbFunctions: mockDb, - pluginManager: { - getLoadedPlugins: mock(() => mockPlugins), - getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), - }, - logger: { - ...logger, - debug: mock(), - error: mock(), - info: mock(), - }, - }); + new Elysia().use(apiConfigRoutes).decorate({ + dbFunctions: mockDb, + pluginManager: { + getLoadedPlugins: mock(() => mockPlugins), + getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), + }, + logger: { + ...logger, + debug: mock(), + error: mock(), + info: mock(), + }, + }); async function captureTestContext( - req: Request, - res: Response, + req: Request, + res: Response ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: string; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch (textError) { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: string; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch (textError) { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("API Configuration Endpoints", () => { - beforeEach(() => { - mockDb.updateConfig.mockClear(); - }); - - describe("Core Configuration", () => { - it("should retrieve current config with hashed API key", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - fetching_interval: expect.any(Number), - keep_data_for: expect.any(Number), - }); - - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with valid config structure", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle config update with valid payload", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const requestBody = { - fetching_interval: 15, - keep_data_for: 30, - api_key: "new-valid-key", - }; - const req = new Request("http://localhost:3000/config/update", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(requestBody), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - success: true, - message: expect.stringContaining("Updated"), - }); - - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Plugin Management", () => { - it("should list active plugins with metadata", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/plugins"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual( - [], - //expect.arrayContaining([ - // expect.objectContaining({ - // name: expect.any(String), - // version: expect.any(String), - // status: expect.any(String), - // }), - //]) - ); - - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with plugin list", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Backup Management", () => { - it("should generate timestamped backup files", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/backup", { - method: "POST", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - const { message } = context.response.body as { message: string }; - expect(message).toMatch( - /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/, - ); - - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with backup path", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should list valid backup files", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/backup"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - const backups = context.response.body as string[]; - expect(backups).toEqual( - expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]), - ); - - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with backup list", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Error Handling", () => { - it("should return proper error format", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/random_link", { - method: "GET", - headers: { "Content-Type": "application/json" }, - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(404); - - recordTestResult({ - name: "should return proper error format", - suite: - "API Configuration Endpoints - Error Handling of unkown routes", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should return proper error format", - suite: "API Configuration Endpoints - Error Handling", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "500 Error with structured error format", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.updateConfig.mockClear(); + }); + + describe("Core Configuration", () => { + it("should retrieve current config with hashed API key", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + fetching_interval: expect.any(Number), + keep_data_for: expect.any(Number), + }); + + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with valid config structure", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle config update with valid payload", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const requestBody = { + fetching_interval: 15, + keep_data_for: 30, + api_key: "new-valid-key", + }; + const req = new Request("http://localhost:3000/config/update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestBody), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + success: true, + message: expect.stringContaining("Updated"), + }); + + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Plugin Management", () => { + it("should list active plugins with metadata", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/plugins"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual( + [] + //expect.arrayContaining([ + // expect.objectContaining({ + // name: expect.any(String), + // version: expect.any(String), + // status: expect.any(String), + // }), + //]) + ); + + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with plugin list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Backup Management", () => { + it("should generate timestamped backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup", { + method: "POST", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const { message } = context.response.body as { message: string }; + expect(message).toMatch( + /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/ + ); + + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup path", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should list valid backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const backups = context.response.body as string[]; + expect(backups).toEqual( + expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]) + ); + + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Error Handling", () => { + it("should return proper error format", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/random_link", { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(404); + + recordTestResult({ + name: "should return proper error format", + suite: + "API Configuration Endpoints - Error Handling of unkown routes", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should return proper error format", + suite: "API Configuration Endpoints - Error Handling", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "500 Error with structured error format", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateJunitReport(); + generateMarkdownReport(); }); diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts index 962937a0..2d1e6ec7 100644 --- a/src/tests/docker-manager.spec.ts +++ b/src/tests/docker-manager.spec.ts @@ -3,462 +3,462 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { dockerRoutes } from "~/routes/docker-manager"; import { - generateJunitReport, - recordTestResult, - testResults, -} from "./junit-exporter"; -import type { TestContext } from "./junit-exporter"; + generateMarkdownReport, + recordTestResult, + testResults, +} from "./markdown-exporter"; +import type { TestContext } from "./markdown-exporter"; type DockerHost = { - id?: number; - name: string; - hostAddress: string; - secure: boolean; + id?: number; + name: string; + hostAddress: string; + secure: boolean; }; const mockDb = { - addDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - updateDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - getDockerHosts: mock(() => []), - deleteDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), + addDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + updateDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + getDockerHosts: mock(() => []), + deleteDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), }; mock.module("~/core/database", () => ({ - dbFunctions: mockDb, + dbFunctions: mockDb, })); mock.module("~/core/utils/logger", () => ({ - logger: { - debug: mock(), - info: mock(), - error: mock(), - }, + logger: { + debug: mock(), + info: mock(), + error: mock(), + }, })); const createApp = () => new Elysia().use(dockerRoutes).decorate({}); async function captureTestContext( - req: Request, - res: Response, + req: Request, + res: Response ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: unknown; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: unknown; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("Docker Configuration Endpoints", () => { - beforeEach(() => { - mockDb.addDockerHost.mockClear(); - mockDb.updateDockerHost.mockClear(); - mockDb.getDockerHosts.mockClear(); - mockDb.deleteDockerHost.mockClear(); - }); - - describe("POST /docker-config/add-host", () => { - it("should add a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host1", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Added docker host (${host.name})`, - }); - expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with success message", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when adding a docker host fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host2", - hostAddress: "invalid", - secure: true, - }; - - // Set mock implementation - mockDb.addDockerHost.mockImplementationOnce(() => { - throw new Error("DB error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error structure", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("POST /docker-config/update-host", () => { - it("should update a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 1, - name: "Host1-upd", - hostAddress: "127.0.0.1:2376", - secure: true, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Updated docker host (${host.id})`, - }); - expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when update fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 2, - name: "Host2", - hostAddress: "x", - secure: false, - }; - - mockDb.updateDockerHost.mockImplementationOnce(() => { - throw new Error("Update error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("GET /docker-config/hosts", () => { - it("should retrieve list of hosts", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const hosts: DockerHost[] = [ - { id: 1, name: "H1", hostAddress: "a", secure: false }, - ]; - - mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual(hosts); - - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with hosts array", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when retrieval fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - mockDb.getDockerHosts.mockImplementationOnce(() => { - throw new Error("Fetch error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("DELETE /docker-config/hosts/:id", () => { - it("should delete a host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 5; - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Deleted docker host (${id})`, - }); - expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); - - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with deletion confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when delete fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 6; - - mockDb.deleteDockerHost.mockImplementationOnce(() => { - throw new Error("Delete error"); - }); - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.addDockerHost.mockClear(); + mockDb.updateDockerHost.mockClear(); + mockDb.getDockerHosts.mockClear(); + mockDb.deleteDockerHost.mockClear(); + }); + + describe("POST /docker-config/add-host", () => { + it("should add a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host1", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Added docker host (${host.name})`, + }); + expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with success message", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when adding a docker host fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host2", + hostAddress: "invalid", + secure: true, + }; + + // Set mock implementation + mockDb.addDockerHost.mockImplementationOnce(() => { + throw new Error("DB error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error structure", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("POST /docker-config/update-host", () => { + it("should update a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 1, + name: "Host1-upd", + hostAddress: "127.0.0.1:2376", + secure: true, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Updated docker host (${host.id})`, + }); + expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when update fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 2, + name: "Host2", + hostAddress: "x", + secure: false, + }; + + mockDb.updateDockerHost.mockImplementationOnce(() => { + throw new Error("Update error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("GET /docker-config/hosts", () => { + it("should retrieve list of hosts", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const hosts: DockerHost[] = [ + { id: 1, name: "H1", hostAddress: "a", secure: false }, + ]; + + mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual(hosts); + + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with hosts array", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when retrieval fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + mockDb.getDockerHosts.mockImplementationOnce(() => { + throw new Error("Fetch error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("DELETE /docker-config/hosts/:id", () => { + it("should delete a host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 5; + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Deleted docker host (${id})`, + }); + expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); + + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with deletion confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when delete fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 6; + + mockDb.deleteDockerHost.mockImplementationOnce(() => { + throw new Error("Delete error"); + }); + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateJunitReport(); + generateMarkdownReport(); }); diff --git a/src/tests/junit-exporter.ts b/src/tests/junit-exporter.ts deleted file mode 100644 index 0ff4bd91..00000000 --- a/src/tests/junit-exporter.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import { format } from "date-fns"; -import { logger } from "~/core/utils/logger"; - -export type TestContext = { - request: { - method: string; - url: string; - headers: Record; - query?: Record; - body?: unknown; - }; - response: { - status: number; - headers: Record; - body?: unknown; - }; -}; - -type ErrorDetails = { - expected?: unknown; - received?: unknown; -}; - -type TestResult = { - name: string; - suite: string; - time: number; - error?: Error; - context?: TestContext; - errorDetails?: ErrorDetails; -}; - -export function recordTestResult(result: TestResult) { - logger.debug(`__UT__ Recording test result: ${JSON.stringify(result)}`); - testResults.push(result); -} - -export let testResults: TestResult[] = []; - -function formatContext( - context?: TestContext, - errorDetails?: ErrorDetails -): string { - if (!context) return ""; - - let output = "=== REQUEST ===\n"; - output += `Method: ${context.request.method}\n`; - output += `URL: ${context.request.url}\n`; - - if (context.request.query) { - output += `Query Params: ${JSON.stringify( - context.request.query, - null, - 2 - )}\n`; - } - - output += `Headers: ${JSON.stringify(context.request.headers, null, 2)}\n`; - - if (context.request.body) { - output += `Body: ${JSON.stringify(context.request.body, null, 2)}\n`; - } - - output += "\n=== RESPONSE ===\n"; - output += `Status: ${context.response.status}\n`; - output += `Headers: ${JSON.stringify(context.response.headers, null, 2)}\n`; - - if (context.response.body) { - output += `Body: ${JSON.stringify(context.response.body, null, 2)}\n`; - } - - if (errorDetails) { - output += "\n=== ERROR DETAILS ===\n"; - output += `Expected: ${JSON.stringify(errorDetails.expected, null, 2)}\n`; - output += `Received: ${JSON.stringify(errorDetails.received, null, 2)}\n`; - } - - return output.replace(/]]>/g, "]]]]>>"); -} - -export function generateJunitReport() { - if (testResults.length === 0) { - logger.warn("No test results to generate JUnit report."); - return; - } - - const totalTests = testResults.length; - const totalErrors = testResults.filter((r) => r.error).length; - - const testSuites = testResults.reduce((suites, result) => { - if (!suites[result.suite]) { - suites[result.suite] = []; - } - suites[result.suite].push(result); - return suites; - }, {} as Record<string, TestResult[]>); - - const xml = `<?xml version="1.0" encoding="UTF-8"?> -<testsuites tests="${totalTests}" errors="${totalErrors}"> - ${Object.entries(testSuites) - .map(([suiteName, cases]) => { - const suiteErrors = cases.filter((c) => c.error).length; - return ` - <testsuite name="${suiteName}" - tests="${cases.length}" - errors="${suiteErrors}" - timestamp="${format(new Date(), "yyyy-MM-dd'T'HH:mm:ss")}"> - ${cases - .map( - (testCase) => ` - <testcase name="${testCase.name}" classname="${suiteName}" time="${ - testCase.time - }"> - ${ - testCase.error - ? ` - <failure message="${testCase.error.message.replace(/"/g, "&quot;")}"> - <![CDATA[${testCase.error.stack?.replace(//g, "]]]]>>")} - ` - : "" - } - - ${formatContext(testCase.context)} - - ` - ) - .join("\n")} - `; - }) - .join("\n")} -`; - - mkdirSync("reports/junit", { recursive: true }); - writeFileSync( - `reports/junit/junit-${format(new Date(), "yyyy-MM-dd")}.xml`, - xml, - "utf8" - ); - - // Clear results after reporting - // resetTestResults(); - - logger.debug(`__UT__ Final data: ${JSON.stringify(testResults)}`); -} diff --git a/src/tests/markdown-exporter.ts b/src/tests/markdown-exporter.ts new file mode 100644 index 00000000..3ebda413 --- /dev/null +++ b/src/tests/markdown-exporter.ts @@ -0,0 +1,141 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { format } from "date-fns"; +import { logger } from "~/core/utils/logger"; + +export type TestContext = { + request: { + method: string; + url: string; + headers: Record; + query?: Record; + body?: unknown; + }; + response: { + status: number; + headers: Record; + body?: unknown; + }; +}; + +type ErrorDetails = { + expected?: unknown; + received?: unknown; +}; + +type TestResult = { + name: string; + suite: string; + time: number; + error?: Error; + context?: TestContext; + errorDetails?: ErrorDetails; +}; + +export function recordTestResult(result: TestResult) { + logger.debug(`__UT__ Recording test result: ${JSON.stringify(result)}`); + testResults.push(result); +} + +export let testResults: TestResult[] = []; + +function formatContextMarkdown( + context?: TestContext, + errorDetails?: ErrorDetails +): string { + if (!context) return ""; + + let md = "```\n"; + md += `=== REQUEST ===\n`; + md += `Method: ${context.request.method}\n`; + md += `URL: ${context.request.url}\n`; + if (context.request.query) { + md += `Query Params: ${JSON.stringify(context.request.query, null, 2)}\n`; + } + md += `Headers: ${JSON.stringify(context.request.headers, null, 2)}\n`; + if (context.request.body) { + md += `Body: ${JSON.stringify(context.request.body, null, 2)}\n`; + } + md += `\n=== RESPONSE ===\n`; + md += `Status: ${context.response.status}\n`; + md += `Headers: ${JSON.stringify(context.response.headers, null, 2)}\n`; + if (context.response.body) { + md += `Body: ${JSON.stringify(context.response.body, null, 2)}\n`; + } + if (errorDetails) { + md += `\n=== ERROR DETAILS ===\n`; + md += `Expected: ${JSON.stringify(errorDetails.expected, null, 2)}\n`; + md += `Received: ${JSON.stringify(errorDetails.received, null, 2)}\n`; + } + md += "```\n"; + return md; +} + +export function generateMarkdownReport() { + if (testResults.length === 0) { + logger.warn("No test results to generate markdown report."); + return; + } + + const totalTests = testResults.length; + const totalErrors = testResults.filter((r) => r.error).length; + + const testSuites = testResults.reduce((suites, result) => { + if (!suites[result.suite]) { + suites[result.suite] = []; + } + suites[result.suite].push(result); + return suites; + }, {} as Record); + + let md = `# Test Report - ${format(new Date(), "yyyy-MM-dd")}\n`; + md += `\n**Total Tests:** ${totalTests} +`; + md += `**Total Failures:** ${totalErrors}\n`; + + for (const [suiteName, cases] of Object.entries(testSuites)) { + const suiteErrors = cases.filter((c) => c.error).length; + md += `\n## Suite: ${suiteName} +`; + md += `- Tests: ${cases.length} +`; + md += `- Failures: ${suiteErrors}\n`; + + for (const test of cases) { + const status = test.error ? "❌ Failed" : "✅ Passed"; + md += `\n### ${test.name} (${(test.time / 1000).toFixed(2)}s) +`; + md += `- Status: **${status}** \n`; + + if (test.error) { + const msg = test.error.message + .replace(//g, ">"); + const stack = test.error.stack + ?.replace(//g, ">"); + md += `\n
\nError Details\n\n`; + md += `**Message:** ${msg} \n`; + if (stack) { + md += `\n\`\`\`\n${stack}\n\`\`\`\n`; + } + md += `
\n`; + } + + if (test.context) { + md += `\n
\nRequest/Response Context\n\n`; + md += formatContextMarkdown(test.context, test.errorDetails); + md += `
\n`; + } + } + } + + // Ensure directory exists + mkdirSync("reports/markdown", { recursive: true }); + const filename = `reports/markdown/test-report-${format( + new Date(), + "yyyy-MM-dd" + )}.md`; + writeFileSync(filename, md, "utf8"); + + logger.debug(`__UT__ Markdown report written to ${filename}`); +} From 9d73c7dbffe18ce778b58cbda7c6e4fa78e7be80 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:28:07 +0200 Subject: [PATCH 318/369] CI/CD: Fix my wrong string flavour :sob: --- .github/workflows/ci.yml | 2 +- src/tests/api-config.spec.ts | 634 +++++++++++----------- src/tests/docker-manager.spec.ts | 866 +++++++++++++++---------------- src/tests/markdown-exporter.ts | 227 ++++---- 4 files changed, 866 insertions(+), 863 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e687e49..42a7875d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: run: | SUMMARY="" for element in $(ls reports/markdown); do - SUMMARY="$(echo -e "${SUMMARY}\n$(cat "${element}")")" + SUMMARY="$(echo -e "${SUMMARY}\n$(cat "reports/markdown${element}")")" done cho "$SUMMARY" >> $GITHUB_STEP_SUMMARY build-scan: diff --git a/src/tests/api-config.spec.ts b/src/tests/api-config.spec.ts index eb62cc83..ba3e7b32 100644 --- a/src/tests/api-config.spec.ts +++ b/src/tests/api-config.spec.ts @@ -6,339 +6,339 @@ import { generateMarkdownReport, recordTestResult } from "./markdown-exporter"; import type { TestContext } from "./markdown-exporter"; const mockDb = { - updateConfig: mock(() => ({})), - backupDatabase: mock( - () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak` - ), - restoreDatabase: mock(), - findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), + updateConfig: mock(() => ({})), + backupDatabase: mock( + () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak`, + ), + restoreDatabase: mock(), + findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), }; mock.module("node:fs", () => ({ - existsSync: mock((path) => path.includes("dockstatapi")), - readdirSync: mock(() => [ - "dockstatapi-2025-05-06.db.bak", - "dockstatapi.db", - "dockstatapi.db-shm", - ]), - unlinkSync: mock(), + existsSync: mock((path) => path.includes("dockstatapi")), + readdirSync: mock(() => [ + "dockstatapi-2025-05-06.db.bak", + "dockstatapi.db", + "dockstatapi.db-shm", + ]), + unlinkSync: mock(), })); const mockPlugins = [ - { - name: "docker-monitor", - version: "1.2.0", - status: "active", - }, + { + name: "docker-monitor", + version: "1.2.0", + status: "active", + }, ]; const createTestApp = () => - new Elysia().use(apiConfigRoutes).decorate({ - dbFunctions: mockDb, - pluginManager: { - getLoadedPlugins: mock(() => mockPlugins), - getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), - }, - logger: { - ...logger, - debug: mock(), - error: mock(), - info: mock(), - }, - }); + new Elysia().use(apiConfigRoutes).decorate({ + dbFunctions: mockDb, + pluginManager: { + getLoadedPlugins: mock(() => mockPlugins), + getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), + }, + logger: { + ...logger, + debug: mock(), + error: mock(), + info: mock(), + }, + }); async function captureTestContext( - req: Request, - res: Response + req: Request, + res: Response, ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: string; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch (textError) { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: string; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch (textError) { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("API Configuration Endpoints", () => { - beforeEach(() => { - mockDb.updateConfig.mockClear(); - }); - - describe("Core Configuration", () => { - it("should retrieve current config with hashed API key", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - fetching_interval: expect.any(Number), - keep_data_for: expect.any(Number), - }); - - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with valid config structure", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle config update with valid payload", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const requestBody = { - fetching_interval: 15, - keep_data_for: 30, - api_key: "new-valid-key", - }; - const req = new Request("http://localhost:3000/config/update", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(requestBody), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - success: true, - message: expect.stringContaining("Updated"), - }); - - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Plugin Management", () => { - it("should list active plugins with metadata", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/plugins"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual( - [] - //expect.arrayContaining([ - // expect.objectContaining({ - // name: expect.any(String), - // version: expect.any(String), - // status: expect.any(String), - // }), - //]) - ); - - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with plugin list", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Backup Management", () => { - it("should generate timestamped backup files", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/backup", { - method: "POST", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - const { message } = context.response.body as { message: string }; - expect(message).toMatch( - /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/ - ); - - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with backup path", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should list valid backup files", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/backup"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - const backups = context.response.body as string[]; - expect(backups).toEqual( - expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]) - ); - - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with backup list", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Error Handling", () => { - it("should return proper error format", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/random_link", { - method: "GET", - headers: { "Content-Type": "application/json" }, - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(404); - - recordTestResult({ - name: "should return proper error format", - suite: - "API Configuration Endpoints - Error Handling of unkown routes", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should return proper error format", - suite: "API Configuration Endpoints - Error Handling", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "500 Error with structured error format", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.updateConfig.mockClear(); + }); + + describe("Core Configuration", () => { + it("should retrieve current config with hashed API key", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + fetching_interval: expect.any(Number), + keep_data_for: expect.any(Number), + }); + + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should retrieve current config with hashed API key", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with valid config structure", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle config update with valid payload", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const requestBody = { + fetching_interval: 15, + keep_data_for: 30, + api_key: "new-valid-key", + }; + const req = new Request("http://localhost:3000/config/update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestBody), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + success: true, + message: expect.stringContaining("Updated"), + }); + + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should handle config update with valid payload", + suite: "API Configuration Endpoints - Core Configuration", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Plugin Management", () => { + it("should list active plugins with metadata", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/plugins"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual( + [], + //expect.arrayContaining([ + // expect.objectContaining({ + // name: expect.any(String), + // version: expect.any(String), + // status: expect.any(String), + // }), + //]) + ); + + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list active plugins with metadata", + suite: "API Configuration Endpoints - Plugin Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with plugin list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Backup Management", () => { + it("should generate timestamped backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup", { + method: "POST", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const { message } = context.response.body as { message: string }; + expect(message).toMatch( + /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/, + ); + + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should generate timestamped backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup path", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should list valid backup files", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/config/backup"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + const backups = context.response.body as string[]; + expect(backups).toEqual( + expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]), + ); + + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should list valid backup files", + suite: "API Configuration Endpoints - Backup Management", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with backup list", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("Error Handling", () => { + it("should return proper error format", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + try { + const app = createTestApp(); + const req = new Request("http://localhost:3000/random_link", { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(404); + + recordTestResult({ + name: "should return proper error format", + suite: + "API Configuration Endpoints - Error Handling of unkown routes", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "should return proper error format", + suite: "API Configuration Endpoints - Error Handling", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "500 Error with structured error format", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateMarkdownReport(); + generateMarkdownReport(); }); diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts index 2d1e6ec7..df9d65da 100644 --- a/src/tests/docker-manager.spec.ts +++ b/src/tests/docker-manager.spec.ts @@ -3,462 +3,462 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { dockerRoutes } from "~/routes/docker-manager"; import { - generateMarkdownReport, - recordTestResult, - testResults, + generateMarkdownReport, + recordTestResult, + testResults, } from "./markdown-exporter"; import type { TestContext } from "./markdown-exporter"; type DockerHost = { - id?: number; - name: string; - hostAddress: string; - secure: boolean; + id?: number; + name: string; + hostAddress: string; + secure: boolean; }; const mockDb = { - addDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - updateDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - getDockerHosts: mock(() => []), - deleteDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), + addDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + updateDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + getDockerHosts: mock(() => []), + deleteDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), }; mock.module("~/core/database", () => ({ - dbFunctions: mockDb, + dbFunctions: mockDb, })); mock.module("~/core/utils/logger", () => ({ - logger: { - debug: mock(), - info: mock(), - error: mock(), - }, + logger: { + debug: mock(), + info: mock(), + error: mock(), + }, })); const createApp = () => new Elysia().use(dockerRoutes).decorate({}); async function captureTestContext( - req: Request, - res: Response + req: Request, + res: Response, ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: unknown; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: unknown; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("Docker Configuration Endpoints", () => { - beforeEach(() => { - mockDb.addDockerHost.mockClear(); - mockDb.updateDockerHost.mockClear(); - mockDb.getDockerHosts.mockClear(); - mockDb.deleteDockerHost.mockClear(); - }); - - describe("POST /docker-config/add-host", () => { - it("should add a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host1", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Added docker host (${host.name})`, - }); - expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with success message", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when adding a docker host fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host2", - hostAddress: "invalid", - secure: true, - }; - - // Set mock implementation - mockDb.addDockerHost.mockImplementationOnce(() => { - throw new Error("DB error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error structure", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("POST /docker-config/update-host", () => { - it("should update a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 1, - name: "Host1-upd", - hostAddress: "127.0.0.1:2376", - secure: true, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Updated docker host (${host.id})`, - }); - expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when update fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 2, - name: "Host2", - hostAddress: "x", - secure: false, - }; - - mockDb.updateDockerHost.mockImplementationOnce(() => { - throw new Error("Update error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("GET /docker-config/hosts", () => { - it("should retrieve list of hosts", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const hosts: DockerHost[] = [ - { id: 1, name: "H1", hostAddress: "a", secure: false }, - ]; - - mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual(hosts); - - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with hosts array", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when retrieval fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - mockDb.getDockerHosts.mockImplementationOnce(() => { - throw new Error("Fetch error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("DELETE /docker-config/hosts/:id", () => { - it("should delete a host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 5; - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Deleted docker host (${id})`, - }); - expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); - - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with deletion confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when delete fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 6; - - mockDb.deleteDockerHost.mockImplementationOnce(() => { - throw new Error("Delete error"); - }); - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.addDockerHost.mockClear(); + mockDb.updateDockerHost.mockClear(); + mockDb.getDockerHosts.mockClear(); + mockDb.deleteDockerHost.mockClear(); + }); + + describe("POST /docker-config/add-host", () => { + it("should add a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host1", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Added docker host (${host.name})`, + }); + expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with success message", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when adding a docker host fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host2", + hostAddress: "invalid", + secure: true, + }; + + // Set mock implementation + mockDb.addDockerHost.mockImplementationOnce(() => { + throw new Error("DB error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error structure", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("POST /docker-config/update-host", () => { + it("should update a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 1, + name: "Host1-upd", + hostAddress: "127.0.0.1:2376", + secure: true, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Updated docker host (${host.id})`, + }); + expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when update fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 2, + name: "Host2", + hostAddress: "x", + secure: false, + }; + + mockDb.updateDockerHost.mockImplementationOnce(() => { + throw new Error("Update error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("GET /docker-config/hosts", () => { + it("should retrieve list of hosts", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const hosts: DockerHost[] = [ + { id: 1, name: "H1", hostAddress: "a", secure: false }, + ]; + + mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual(hosts); + + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with hosts array", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when retrieval fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + mockDb.getDockerHosts.mockImplementationOnce(() => { + throw new Error("Fetch error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("DELETE /docker-config/hosts/:id", () => { + it("should delete a host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 5; + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Deleted docker host (${id})`, + }); + expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); + + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with deletion confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when delete fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 6; + + mockDb.deleteDockerHost.mockImplementationOnce(() => { + throw new Error("Delete error"); + }); + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + error: expect.any(String), + }); + + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateMarkdownReport(); + generateMarkdownReport(); }); diff --git a/src/tests/markdown-exporter.ts b/src/tests/markdown-exporter.ts index 3ebda413..2d55b48e 100644 --- a/src/tests/markdown-exporter.ts +++ b/src/tests/markdown-exporter.ts @@ -3,139 +3,142 @@ import { format } from "date-fns"; import { logger } from "~/core/utils/logger"; export type TestContext = { - request: { - method: string; - url: string; - headers: Record; - query?: Record; - body?: unknown; - }; - response: { - status: number; - headers: Record; - body?: unknown; - }; + request: { + method: string; + url: string; + headers: Record; + query?: Record; + body?: unknown; + }; + response: { + status: number; + headers: Record; + body?: unknown; + }; }; type ErrorDetails = { - expected?: unknown; - received?: unknown; + expected?: unknown; + received?: unknown; }; type TestResult = { - name: string; - suite: string; - time: number; - error?: Error; - context?: TestContext; - errorDetails?: ErrorDetails; + name: string; + suite: string; + time: number; + error?: Error; + context?: TestContext; + errorDetails?: ErrorDetails; }; export function recordTestResult(result: TestResult) { - logger.debug(`__UT__ Recording test result: ${JSON.stringify(result)}`); - testResults.push(result); + logger.debug(`__UT__ Recording test result: ${JSON.stringify(result)}`); + testResults.push(result); } -export let testResults: TestResult[] = []; +export const testResults: TestResult[] = []; function formatContextMarkdown( - context?: TestContext, - errorDetails?: ErrorDetails + context?: TestContext, + errorDetails?: ErrorDetails, ): string { - if (!context) return ""; - - let md = "```\n"; - md += `=== REQUEST ===\n`; - md += `Method: ${context.request.method}\n`; - md += `URL: ${context.request.url}\n`; - if (context.request.query) { - md += `Query Params: ${JSON.stringify(context.request.query, null, 2)}\n`; - } - md += `Headers: ${JSON.stringify(context.request.headers, null, 2)}\n`; - if (context.request.body) { - md += `Body: ${JSON.stringify(context.request.body, null, 2)}\n`; - } - md += `\n=== RESPONSE ===\n`; - md += `Status: ${context.response.status}\n`; - md += `Headers: ${JSON.stringify(context.response.headers, null, 2)}\n`; - if (context.response.body) { - md += `Body: ${JSON.stringify(context.response.body, null, 2)}\n`; - } - if (errorDetails) { - md += `\n=== ERROR DETAILS ===\n`; - md += `Expected: ${JSON.stringify(errorDetails.expected, null, 2)}\n`; - md += `Received: ${JSON.stringify(errorDetails.received, null, 2)}\n`; - } - md += "```\n"; - return md; + if (!context) return ""; + + let md = "```\n"; + md += "=== REQUEST ===\n"; + md += `Method: ${context.request.method}\n`; + md += `URL: ${context.request.url}\n`; + if (context.request.query) { + md += `Query Params: ${JSON.stringify(context.request.query, null, 2)}\n`; + } + md += `Headers: ${JSON.stringify(context.request.headers, null, 2)}\n`; + if (context.request.body) { + md += `Body: ${JSON.stringify(context.request.body, null, 2)}\n`; + } + md += "\n=== RESPONSE ===\n"; + md += `Status: ${context.response.status}\n`; + md += `Headers: ${JSON.stringify(context.response.headers, null, 2)}\n`; + if (context.response.body) { + md += `Body: ${JSON.stringify(context.response.body, null, 2)}\n`; + } + if (errorDetails) { + md += "\n=== ERROR DETAILS ===\n"; + md += `Expected: ${JSON.stringify(errorDetails.expected, null, 2)}\n`; + md += `Received: ${JSON.stringify(errorDetails.received, null, 2)}\n`; + } + md += "```\n"; + return md; } export function generateMarkdownReport() { - if (testResults.length === 0) { - logger.warn("No test results to generate markdown report."); - return; - } - - const totalTests = testResults.length; - const totalErrors = testResults.filter((r) => r.error).length; - - const testSuites = testResults.reduce((suites, result) => { - if (!suites[result.suite]) { - suites[result.suite] = []; - } - suites[result.suite].push(result); - return suites; - }, {} as Record); - - let md = `# Test Report - ${format(new Date(), "yyyy-MM-dd")}\n`; - md += `\n**Total Tests:** ${totalTests} + if (testResults.length === 0) { + logger.warn("No test results to generate markdown report."); + return; + } + + const totalTests = testResults.length; + const totalErrors = testResults.filter((r) => r.error).length; + + const testSuites = testResults.reduce( + (suites, result) => { + if (!suites[result.suite]) { + suites[result.suite] = []; + } + suites[result.suite].push(result); + return suites; + }, + {} as Record, + ); + + let md = `# Test Report - ${format(new Date(), "yyyy-MM-dd")}\n`; + md += `\n**Total Tests:** ${totalTests} `; - md += `**Total Failures:** ${totalErrors}\n`; + md += `**Total Failures:** ${totalErrors}\n`; - for (const [suiteName, cases] of Object.entries(testSuites)) { - const suiteErrors = cases.filter((c) => c.error).length; - md += `\n## Suite: ${suiteName} + for (const [suiteName, cases] of Object.entries(testSuites)) { + const suiteErrors = cases.filter((c) => c.error).length; + md += `\n## Suite: ${suiteName} `; - md += `- Tests: ${cases.length} + md += `- Tests: ${cases.length} `; - md += `- Failures: ${suiteErrors}\n`; + md += `- Failures: ${suiteErrors}\n`; - for (const test of cases) { - const status = test.error ? "❌ Failed" : "✅ Passed"; - md += `\n### ${test.name} (${(test.time / 1000).toFixed(2)}s) + for (const test of cases) { + const status = test.error ? "❌ Failed" : "✅ Passed"; + md += `\n### ${test.name} (${(test.time / 1000).toFixed(2)}s) `; - md += `- Status: **${status}** \n`; - - if (test.error) { - const msg = test.error.message - .replace(//g, ">"); - const stack = test.error.stack - ?.replace(//g, ">"); - md += `\n
\nError Details\n\n`; - md += `**Message:** ${msg} \n`; - if (stack) { - md += `\n\`\`\`\n${stack}\n\`\`\`\n`; - } - md += `
\n`; - } - - if (test.context) { - md += `\n
\nRequest/Response Context\n\n`; - md += formatContextMarkdown(test.context, test.errorDetails); - md += `
\n`; - } - } - } - - // Ensure directory exists - mkdirSync("reports/markdown", { recursive: true }); - const filename = `reports/markdown/test-report-${format( - new Date(), - "yyyy-MM-dd" - )}.md`; - writeFileSync(filename, md, "utf8"); - - logger.debug(`__UT__ Markdown report written to ${filename}`); + md += `- Status: **${status}** \n`; + + if (test.error) { + const msg = test.error.message + .replace(//g, ">"); + const stack = test.error.stack + ?.replace(//g, ">"); + md += "\n
\nError Details\n\n"; + md += `**Message:** ${msg} \n`; + if (stack) { + md += `\n\`\`\`\n${stack}\n\`\`\`\n`; + } + md += "
\n"; + } + + if (test.context) { + md += "\n
\nRequest/Response Context\n\n"; + md += formatContextMarkdown(test.context, test.errorDetails); + md += "
\n"; + } + } + } + + // Ensure directory exists + mkdirSync("reports/markdown", { recursive: true }); + const filename = `reports/markdown/test-report-${format( + new Date(), + "yyyy-MM-dd", + )}.md`; + writeFileSync(filename, md, "utf8"); + + logger.debug(`__UT__ Markdown report written to ${filename}`); } From dae4d078234d278f50d15d3243b98f44a0a03652 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:29:27 +0200 Subject: [PATCH 319/369] CI/CD: Fix wrong path in for loop --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42a7875d..56f8e78f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: run: | SUMMARY="" for element in $(ls reports/markdown); do - SUMMARY="$(echo -e "${SUMMARY}\n$(cat "reports/markdown${element}")")" + SUMMARY="$(echo -e "${SUMMARY}\n$(cat "reports/markdown/${element}")")" done cho "$SUMMARY" >> $GITHUB_STEP_SUMMARY build-scan: From c8dbcbd4f19629e0d37bfecdfc5449bdbfbd7332 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:30:51 +0200 Subject: [PATCH 320/369] CI/CD: fix type --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56f8e78f..6cf46283 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: for element in $(ls reports/markdown); do SUMMARY="$(echo -e "${SUMMARY}\n$(cat "reports/markdown/${element}")")" done - cho "$SUMMARY" >> $GITHUB_STEP_SUMMARY + echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY build-scan: name: Build and Security Scan runs-on: ubuntu-latest From 17b1f98a46b69964520be1265639883301bf675b Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 14 May 2025 17:40:26 +0200 Subject: [PATCH 321/369] CI/CD: Add CONTRIBUTORS.md workflow --- .github/workflows/contributors.yml | 21 +++++++++++++++++++++ CONTRIBUTORS.md | 0 2 files changed, 21 insertions(+) create mode 100644 .github/workflows/contributors.yml create mode 100644 CONTRIBUTORS.md diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml new file mode 100644 index 00000000..9edece27 --- /dev/null +++ b/.github/workflows/contributors.yml @@ -0,0 +1,21 @@ +name: Update CONTRIBUTORS file +on: + schedule: + - cron: "0 0 1 * *" + workflow_dispatch: +jobs: + main: + runs-on: ubuntu-latest + steps: + - uses: minicli/action-contributors@v3.3 + name: "Update a projects CONTRIBUTORS file" + env: + CONTRIB_REPOSITORY: "Its4Nik/DockStatAPI" + CONTRIB_OUTPUT_FILE: "CONTRIBUTORS.md" + + - name: Commit changes + uses: test-room-7/action-update-file@v1 + with: + file-path: "CONTRIBUTORS.md" + commit-msg: Update Contributors + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 00000000..e69de29b From 9a455d961a17e861820d859f2832eaf951268c48 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 16 May 2025 10:32:54 +0200 Subject: [PATCH 322/369] Feat: Adjust error responses --- docker/docker-compose.dev.yaml | 56 +- src/core/utils/response-handler.ts | 69 +- src/routes/api-config.ts | 1114 +++++++++++++-------------- src/routes/docker-manager.ts | 504 ++++++------ src/routes/docker-stats.ts | 1135 ++++++++++++++-------------- src/tests/docker-manager.spec.ts | 866 ++++++++++----------- 6 files changed, 1863 insertions(+), 1881 deletions(-) diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index 799ae7a8..f302c585 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -12,34 +12,34 @@ services: ports: - 2375:2375 environment: - - ALLOW_START=1 #optional - - ALLOW_STOP=1 #optional - - ALLOW_RESTARTS=1 #optional - - AUTH=1 #optional - - BUILD=1 #optional - - COMMIT=1 #optional - - CONFIGS=1 #optional - - CONTAINERS=1 #optional - - DISABLE_IPV6=1 #optional - - DISTRIBUTION=1 #optional - - EVENTS=1 #optional - - EXEC=1 #optional - - IMAGES=1 #optional - - INFO=1 #optional - - NETWORKS=1 #optional - - NODES=1 #optional - - PING=1 #optional - - PLUGINS=1 #optional - - POST=1 #optional - - PROXY_READ_TIMEOUT=240 #optional - - SECRETS=1 #optional - - SERVICES=1 #optional - - SESSION=1 #optional - - SWARM=1 #optional - - SYSTEM=1 #optional - - TASKS=1 #optional - - VERSION=1 #optional - - VOLUMES=1 #optional + - ALLOW_START=1 + - ALLOW_STOP=1 + - ALLOW_RESTARTS=1 + - AUTH=1 + - BUILD=1 + - COMMIT=1 + - CONFIGS=1 + - CONTAINERS=1 + - DISABLE_IPV6=1 + - DISTRIBUTION=1 + - EVENTS=1 + - EXEC=1 + - IMAGES=1 + - INFO=1 + - NETWORKS=1 + - NODES=1 + - PING=1 + - PLUGINS=1 + - POST=1 + - PROXY_READ_TIMEOUT=240 + - SECRETS=1 + - SERVICES=1 + - SESSION=1 + - SWARM=1 + - SYSTEM=1 + - TASKS=1 + - VERSION=1 + - VOLUMES=1 sqlite-web: container_name: sqlite-web diff --git a/src/core/utils/response-handler.ts b/src/core/utils/response-handler.ts index 8bfe6ec3..990ae818 100644 --- a/src/core/utils/response-handler.ts +++ b/src/core/utils/response-handler.ts @@ -1,43 +1,42 @@ import { logger } from "~/core/utils/logger"; - import type { set } from "~/typings/elysiajs"; export const responseHandler = { - error( - set: set, - error: string, - response_message: string, - error_code?: number, - ) { - set.status = error_code || 500; - logger.error(`${response_message} - ${error}`); - return { error: `${response_message}` }; - }, + error( + set: set, + error: string, + response_message: string, + error_code?: number + ) { + set.status = error_code || 500; + logger.error(`${response_message} - ${error}`); + return { success: false, message: response_message, error: String(error) }; + }, - ok(set: set, response_message: string) { - set.status = 200; - logger.debug(response_message); - return { success: true, message: response_message }; - }, + ok(set: set, response_message: string) { + set.status = 200; + logger.debug(response_message); + return { success: true, message: response_message }; + }, - simple_error(set: set, response_message: string, status_code?: number) { - set.status = status_code || 502; - logger.warn(response_message); - return { error: response_message }; - }, + simple_error(set: set, response_message: string, status_code?: number) { + set.status = status_code || 502; + logger.warn(response_message); + return { success: false, message: response_message }; + }, - reject( - set: set, - reject: CallableFunction, - response_message: string, - error?: string, - ) { - set.status = 501; - if (error) { - logger.error(`${response_message} - ${error}`); - } else { - logger.error(response_message); - } - return reject(new Error(response_message)); - }, + reject( + set: set, + reject: CallableFunction, + response_message: string, + error?: string + ) { + set.status = 501; + if (error) { + logger.error(`${response_message} - ${error}`); + } else { + logger.error(response_message); + } + return reject(new Error(response_message)); + }, }; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 8759505a..bfbaa753 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -4,583 +4,583 @@ import { dbFunctions } from "~/core/database"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import { responseHandler } from "~/core/utils/response-handler"; - import { backupDir } from "~/core/database/backup"; import { hashApiKey } from "~/middleware/auth"; import type { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; + .get( + "", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - responses: { - "200": { - description: "Successfully retrieved configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - fetching_interval: { - type: "number", - example: 5, - }, - keep_data_for: { - type: "number", - example: 7, - }, - api_key: { - type: "string", - example: "hashed_api_key", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/plugins", - ({ set }) => { - try { - return pluginManager.getLoadedPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - responses: { - "200": { - description: "Successfully retrieved plugins", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-plugin", - }, - version: { - type: "string", - example: "1.0.0", - }, - status: { - type: "string", - example: "active", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving plugins", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting all registered plugins", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Returns current API configuration including data retention policies and security settings", + responses: { + "200": { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fetching_interval: { + type: "number", + example: 5, + }, + keep_data_for: { + type: "number", + example: 7, + }, + api_key: { + type: "string", + example: "hashed_api_key", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/plugins", + ({ set }) => { + try { + return pluginManager.getLoadedPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + responses: { + "200": { + description: "Successfully retrieved plugins", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-plugin", + }, + version: { + type: "string", + example: "1.0.0", + }, + status: { + type: "string", + example: "active", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving plugins", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting all registered plugins", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { fetching_interval, keep_data_for, api_key } = body; - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key), - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - responses: { - "200": { - description: "Successfully updated configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated DockStatAPI config", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error updating the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - }, - ) - .get( - "/package", - async () => { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key) + ); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", + responses: { + "200": { + description: "Successfully updated configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated DockStatAPI config", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error updating the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), + } + ) + .get( + "/package", + async () => { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json`, - ); + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json` + ); - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - responses: { - "200": { - description: "Successfully retrieved package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error while reading package.json", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .post( - "/backup", - async ({ set }) => { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return responseHandler.ok(set, backupFilename); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Backs up the internal database", - responses: { - "200": { - description: "Successfully created backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "backup_2024-03-20_12-00-00.db.bak", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error creating backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error backing up", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/backup", - async ({ set }) => { - try { - const backupFiles = readdirSync(backupDir); + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Displays package metadata including dependencies, contributors, and licensing information", + responses: { + "200": { + description: "Successfully retrieved package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0", + }, + description: { + type: "string", + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", + }, + license: { + type: "string", + example: "CC BY-NC 4.0", + }, + authorName: { + type: "string", + example: "ItsNik", + }, + authorEmail: { + type: "string", + example: "info@itsnik.de", + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik", + }, + contributors: { + type: "array", + items: { + type: "string", + }, + example: [], + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + }, + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error while reading package.json", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .post( + "/backup", + async ({ set }) => { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return responseHandler.ok(set, backupFilename); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Backs up the internal database", + responses: { + "200": { + description: "Successfully created backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "backup_2024-03-20_12-00-00.db.bak", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error creating backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error backing up", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/backup", + async ({ set }) => { + try { + const backupFiles = readdirSync(backupDir); - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Lists all available backups", - responses: { - "200": { - description: "Successfully retrieved backup list", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "string", - }, - example: [ - "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak", - ], - }, - }, - }, - }, - "400": { - description: "Error retrieving backup list", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Reading Backup directory", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Lists all available backups", + responses: { + "200": { + description: "Successfully retrieved backup list", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "string", + }, + example: [ + "backup_2024-03-20_12-00-00.db.bak", + "backup_2024-03-19_12-00-00.db.bak", + ], + }, + }, + }, + }, + "400": { + description: "Error retrieving backup list", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Reading Backup directory", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) - .get( - "/backup/download", - async ({ query, set }) => { - try { - const filename = query.filename || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; + .get( + "/backup/download", + async ({ query, set }) => { + try { + const filename = query.filename || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } - set.headers["Content-Type"] = "application/octet-stream"; - set.headers["Content-Disposition"] = - `attachment; filename="${filename}"`; - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Download a specific backup or the latest if no filename is provided", - responses: { - "200": { - description: "Successfully downloaded backup file", - content: { - "application/octet-stream": { - schema: { - type: "string", - format: "binary", - example: "Binary backup file content", - }, - }, - }, - headers: { - "Content-Disposition": { - schema: { - type: "string", - example: - 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', - }, - }, - }, - }, - "400": { - description: "Error downloading backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Backup download failed", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - filename: t.Optional(t.String()), - }), - }, - ) - .post( - "/restore", - async ({ body, set }) => { - try { - const { file } = body; + set.headers["Content-Type"] = "application/octet-stream"; + set.headers[ + "Content-Disposition" + ] = `attachment; filename="${filename}"`; + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Download a specific backup or the latest if no filename is provided", + responses: { + "200": { + description: "Successfully downloaded backup file", + content: { + "application/octet-stream": { + schema: { + type: "string", + format: "binary", + example: "Binary backup file content", + }, + }, + }, + headers: { + "Content-Disposition": { + schema: { + type: "string", + example: + 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', + }, + }, + }, + }, + "400": { + description: "Error downloading backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Backup download failed", + }, + }, + }, + }, + }, + }, + }, + }, + query: t.Object({ + filename: t.Optional(t.String()), + }), + } + ) + .post( + "/restore", + async ({ body, set }) => { + try { + const { file } = body; - set.headers["Content-Type"] = "text/html"; + set.headers["Content-Type"] = "text/html"; - if (!file) { - throw new Error("No file uploaded"); - } + if (!file) { + throw new Error("No file uploaded"); + } - if (!file.name.endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } + if (!file.name.endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); - return responseHandler.ok(set, "Database restored successfully"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - body: t.Object({ file: t.File() }), - detail: { - tags: ["Management"], - description: "Restore database from uploaded backup file", - responses: { - "200": { - description: "Successfully restored database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Database restored successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restoring database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Database restoration error", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); + return responseHandler.ok(set, "Database restored successfully"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + body: t.Object({ file: t.File() }), + detail: { + tags: ["Management"], + description: "Restore database from uploaded backup file", + responses: { + "200": { + description: "Successfully restored database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Database restored successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error restoring database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Database restoration error", + }, + }, + }, + }, + }, + }, + }, + }, + } + ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 30cd5c46..279fa2ba 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -1,269 +1,255 @@ import { Elysia, t } from "elysia"; - +import type { DockerHost } from "~/typings/docker"; import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; -import type { DockerHost } from "~/typings/docker"; - export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) - .post( - "/add-host", - async ({ set, body }) => { - try { - dbFunctions.addDockerHost(body as DockerHost); - return responseHandler.ok(set, `Added docker host (${body.name})`); - } catch (error: unknown) { - return responseHandler.error( - set, - "Error adding docker Host", - error as string, - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Registers a new Docker host to the monitoring system with connection details", - responses: { - "200": { - description: "Successfully added Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Added docker host (Localhost)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error adding Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error adding docker Host", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - name: t.String(), - hostAddress: t.String(), - secure: t.Boolean(), - }), - }, - ) + .post( + "/add-host", + async ({ set, body }) => { + try { + dbFunctions.addDockerHost(body as DockerHost); + return responseHandler.ok(set, `Added docker host (${body.name})`); + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Registers a new Docker host to the monitoring system with connection details", + responses: { + "200": { + description: "Successfully added Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Added docker host (Localhost)", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error adding Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error adding docker Host", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + name: t.String(), + hostAddress: t.String(), + secure: t.Boolean(), + }), + } + ) - .post( - "/update-host", - async ({ set, body }) => { - try { - set.status = 200; - dbFunctions.updateDockerHost(body); - return responseHandler.ok(set, `Updated docker host (${body.id})`); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to update host", - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies existing Docker host configuration parameters (name, address, security)", - responses: { - "200": { - description: "Successfully updated Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated docker host (1)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to update host", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - id: t.Number(), - name: t.String(), - hostAddress: t.String(), - secure: t.Boolean(), - }), - }, - ) + .post( + "/update-host", + async ({ set, body }) => { + try { + set.status = 200; + dbFunctions.updateDockerHost(body); + return responseHandler.ok(set, `Updated docker host (${body.id})`); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies existing Docker host configuration parameters (name, address, security)", + responses: { + "200": { + description: "Successfully updated Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated docker host (1)", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to update host", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + id: t.Number(), + name: t.String(), + hostAddress: t.String(), + secure: t.Boolean(), + }), + } + ) - .get( - "/hosts", - async ({ set }) => { - try { - const dockerHosts = dbFunctions.getDockerHosts(); + .get( + "/hosts", + async ({ set }) => { + try { + const dockerHosts = dbFunctions.getDockerHosts(); - logger.debug("Retrieved docker hosts"); - return dockerHosts; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve hosts", - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all configured Docker hosts with their connection settings", - responses: { - "200": { - description: "Successfully retrieved Docker hosts", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "number", - example: 1, - }, - name: { - type: "string", - example: "Localhost", - }, - hostAddress: { - type: "string", - example: "localhost:2375", - }, - secure: { - type: "boolean", - example: false, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving Docker hosts", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve hosts", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) + logger.debug("Retrieved docker hosts"); + return dockerHosts; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all configured Docker hosts with their connection settings", + responses: { + "200": { + description: "Successfully retrieved Docker hosts", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "number", + example: 1, + }, + name: { + type: "string", + example: "Localhost", + }, + hostAddress: { + type: "string", + example: "localhost:2375", + }, + secure: { + type: "boolean", + example: false, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving Docker hosts", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve hosts", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) - .delete( - "/hosts/:id", - async ({ set, params }) => { - try { - set.status = 200; - dbFunctions.deleteDockerHost(params.id); - return responseHandler.ok(set, `Deleted docker host (${params.id})`); - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to delete host", - ); - } - }, - { - detail: { - tags: ["Management"], - description: - "Removes Docker host from monitoring system and clears associated data", - responses: { - "200": { - description: "Successfully deleted Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Deleted docker host (1)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error deleting Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to delete host", - }, - }, - }, - }, - }, - }, - }, - }, - params: t.Object({ - id: t.Number(), - }), - }, - ); + .delete( + "/hosts/:id", + async ({ set, params }) => { + try { + set.status = 200; + dbFunctions.deleteDockerHost(params.id); + return responseHandler.ok(set, `Deleted docker host (${params.id})`); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Removes Docker host from monitoring system and clears associated data", + responses: { + "200": { + description: "Successfully deleted Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Deleted docker host (1)", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error deleting Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to delete host", + }, + }, + }, + }, + }, + }, + }, + }, + params: t.Object({ + id: t.Number(), + }), + } + ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index b411f2d4..f9d966d6 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -1,601 +1,598 @@ import type Docker from "dockerode"; import { Elysia } from "elysia"; - import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; - import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get( - "/containers", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; + .get( + "/containers", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - return responseHandler.error( - set, - pingError as string, - "Docker host connection failed", - ); - } + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + return responseHandler.error( + set, + pingError as string, + "Docker host connection failed" + ); + } - const hostContainers = await docker.listContainers({ all: true }); + const hostContainers = await docker.listContainers({ all: true }); - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - return responseHandler.reject( - set, - reject, - "An error occurred", - error, - ); - } - if (!stats) { - return responseHandler.reject( - set, - reject, - "No stats available", - ); - } - resolve(stats); - }); - }, - ); + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return responseHandler.reject( + set, + reject, + "An error occurred", + error + ); + } + if (!stats) { + return responseHandler.reject( + set, + reject, + "No stats available" + ); + } + resolve(stats); + }); + } + ); - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError, - ); - } - }), - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (hostError) { - logger.error("Error fetching containers for host,", hostError); - } - }), - ); + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError + ); + } + }) + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }) + ); - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve containers", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", - responses: { - "200": { - description: "Successfully retrieved container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - containers: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "string", - example: "abc123def456", - }, - hostId: { - type: "string", - example: "1", - }, - name: { - type: "string", - example: "example-container", - }, - image: { - type: "string", - example: "nginx:latest", - }, - status: { - type: "string", - example: "running", - }, - state: { - type: "string", - example: "running", - }, - cpuUsage: { - type: "number", - example: 0.5, - }, - memoryUsage: { - type: "number", - example: 1024, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve containers", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", + responses: { + "200": { + description: "Successfully retrieved container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + containers: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + example: "abc123def456", + }, + hostId: { + type: "string", + example: "1", + }, + name: { + type: "string", + example: "example-container", + }, + image: { + type: "string", + example: "nginx:latest", + }, + status: { + type: "string", + example: "running", + }, + state: { + type: "string", + example: "running", + }, + cpuUsage: { + type: "number", + example: 0.5, + }, + memoryUsage: { + type: "number", + example: 1024, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve containers", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - logger.debug("Fetched all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - logger.debug("Fetched stats for all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for all hosts", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/hosts/:id", - async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched stats for all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for all hosts", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/hosts/:id", + async ({ params, set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = findObjectByKey(hosts, "id", Number(params.id)); - if (!host) { - return responseHandler.simple_error( - set, - `Host (${params.id}) not found`, - ); - } + const host = findObjectByKey(hosts, "id", Number(params.id)); + if (!host) { + return responseHandler.simple_error( + set, + `Host (${params.id}) not found` + ); + } - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ); diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts index df9d65da..64e04d22 100644 --- a/src/tests/docker-manager.spec.ts +++ b/src/tests/docker-manager.spec.ts @@ -3,462 +3,462 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { dockerRoutes } from "~/routes/docker-manager"; import { - generateMarkdownReport, - recordTestResult, - testResults, + generateMarkdownReport, + recordTestResult, + testResults, } from "./markdown-exporter"; import type { TestContext } from "./markdown-exporter"; type DockerHost = { - id?: number; - name: string; - hostAddress: string; - secure: boolean; + id?: number; + name: string; + hostAddress: string; + secure: boolean; }; const mockDb = { - addDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - updateDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - getDockerHosts: mock(() => []), - deleteDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), + addDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + updateDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + getDockerHosts: mock(() => []), + deleteDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), }; mock.module("~/core/database", () => ({ - dbFunctions: mockDb, + dbFunctions: mockDb, })); mock.module("~/core/utils/logger", () => ({ - logger: { - debug: mock(), - info: mock(), - error: mock(), - }, + logger: { + debug: mock(), + info: mock(), + error: mock(), + }, })); const createApp = () => new Elysia().use(dockerRoutes).decorate({}); async function captureTestContext( - req: Request, - res: Response, + req: Request, + res: Response ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: unknown; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: unknown; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("Docker Configuration Endpoints", () => { - beforeEach(() => { - mockDb.addDockerHost.mockClear(); - mockDb.updateDockerHost.mockClear(); - mockDb.getDockerHosts.mockClear(); - mockDb.deleteDockerHost.mockClear(); - }); - - describe("POST /docker-config/add-host", () => { - it("should add a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host1", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Added docker host (${host.name})`, - }); - expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with success message", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when adding a docker host fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host2", - hostAddress: "invalid", - secure: true, - }; - - // Set mock implementation - mockDb.addDockerHost.mockImplementationOnce(() => { - throw new Error("DB error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error structure", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("POST /docker-config/update-host", () => { - it("should update a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 1, - name: "Host1-upd", - hostAddress: "127.0.0.1:2376", - secure: true, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Updated docker host (${host.id})`, - }); - expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when update fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 2, - name: "Host2", - hostAddress: "x", - secure: false, - }; - - mockDb.updateDockerHost.mockImplementationOnce(() => { - throw new Error("Update error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("GET /docker-config/hosts", () => { - it("should retrieve list of hosts", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const hosts: DockerHost[] = [ - { id: 1, name: "H1", hostAddress: "a", secure: false }, - ]; - - mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual(hosts); - - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with hosts array", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when retrieval fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - mockDb.getDockerHosts.mockImplementationOnce(() => { - throw new Error("Fetch error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("DELETE /docker-config/hosts/:id", () => { - it("should delete a host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 5; - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Deleted docker host (${id})`, - }); - expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); - - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with deletion confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when delete fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 6; - - mockDb.deleteDockerHost.mockImplementationOnce(() => { - throw new Error("Delete error"); - }); - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - error: expect.any(String), - }); - - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.addDockerHost.mockClear(); + mockDb.updateDockerHost.mockClear(); + mockDb.getDockerHosts.mockClear(); + mockDb.deleteDockerHost.mockClear(); + }); + + describe("POST /docker-config/add-host", () => { + it("should add a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host1", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Added docker host (${host.name})`, + }); + expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with success message", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when adding a docker host fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host2", + hostAddress: "invalid", + secure: true, + }; + + // Set mock implementation + mockDb.addDockerHost.mockImplementationOnce(() => { + throw new Error("DB error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error structure", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("POST /docker-config/update-host", () => { + it("should update a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 1, + name: "Host1-upd", + hostAddress: "127.0.0.1:2376", + secure: true, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Updated docker host (${host.id})`, + }); + expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when update fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 2, + name: "Host2", + hostAddress: "x", + secure: false, + }; + + mockDb.updateDockerHost.mockImplementationOnce(() => { + throw new Error("Update error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("GET /docker-config/hosts", () => { + it("should retrieve list of hosts", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const hosts: DockerHost[] = [ + { id: 1, name: "H1", hostAddress: "a", secure: false }, + ]; + + mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual(hosts); + + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with hosts array", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when retrieval fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + mockDb.getDockerHosts.mockImplementationOnce(() => { + throw new Error("Fetch error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("DELETE /docker-config/hosts/:id", () => { + it("should delete a host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 5; + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Deleted docker host (${id})`, + }); + expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); + + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with deletion confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when delete fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 6; + + mockDb.deleteDockerHost.mockImplementationOnce(() => { + throw new Error("Delete error"); + }); + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateMarkdownReport(); + generateMarkdownReport(); }); From 7bb328b0f4a8770130e71eddc419bd926b7af90f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 16 May 2025 08:33:33 +0000 Subject: [PATCH 323/369] CQL: Apply lint fixes [skip ci] --- src/core/utils/response-handler.ts | 68 +- src/routes/api-config.ts | 1115 ++++++++++++++------------- src/routes/docker-manager.ts | 490 ++++++------ src/routes/docker-stats.ts | 1132 ++++++++++++++-------------- src/tests/docker-manager.spec.ts | 866 ++++++++++----------- 5 files changed, 1835 insertions(+), 1836 deletions(-) diff --git a/src/core/utils/response-handler.ts b/src/core/utils/response-handler.ts index 990ae818..00d5b464 100644 --- a/src/core/utils/response-handler.ts +++ b/src/core/utils/response-handler.ts @@ -2,41 +2,41 @@ import { logger } from "~/core/utils/logger"; import type { set } from "~/typings/elysiajs"; export const responseHandler = { - error( - set: set, - error: string, - response_message: string, - error_code?: number - ) { - set.status = error_code || 500; - logger.error(`${response_message} - ${error}`); - return { success: false, message: response_message, error: String(error) }; - }, + error( + set: set, + error: string, + response_message: string, + error_code?: number, + ) { + set.status = error_code || 500; + logger.error(`${response_message} - ${error}`); + return { success: false, message: response_message, error: String(error) }; + }, - ok(set: set, response_message: string) { - set.status = 200; - logger.debug(response_message); - return { success: true, message: response_message }; - }, + ok(set: set, response_message: string) { + set.status = 200; + logger.debug(response_message); + return { success: true, message: response_message }; + }, - simple_error(set: set, response_message: string, status_code?: number) { - set.status = status_code || 502; - logger.warn(response_message); - return { success: false, message: response_message }; - }, + simple_error(set: set, response_message: string, status_code?: number) { + set.status = status_code || 502; + logger.warn(response_message); + return { success: false, message: response_message }; + }, - reject( - set: set, - reject: CallableFunction, - response_message: string, - error?: string - ) { - set.status = 501; - if (error) { - logger.error(`${response_message} - ${error}`); - } else { - logger.error(response_message); - } - return reject(new Error(response_message)); - }, + reject( + set: set, + reject: CallableFunction, + response_message: string, + error?: string, + ) { + set.status = 501; + if (error) { + logger.error(`${response_message} - ${error}`); + } else { + logger.error(response_message); + } + return reject(new Error(response_message)); + }, }; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index bfbaa753..6e2de1eb 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -1,586 +1,585 @@ import { existsSync, readdirSync, unlinkSync } from "node:fs"; import { Elysia, t } from "elysia"; import { dbFunctions } from "~/core/database"; +import { backupDir } from "~/core/database/backup"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import { responseHandler } from "~/core/utils/response-handler"; -import { backupDir } from "~/core/database/backup"; import { hashApiKey } from "~/middleware/auth"; import type { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; + .get( + "", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - responses: { - "200": { - description: "Successfully retrieved configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - fetching_interval: { - type: "number", - example: 5, - }, - keep_data_for: { - type: "number", - example: 7, - }, - api_key: { - type: "string", - example: "hashed_api_key", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/plugins", - ({ set }) => { - try { - return pluginManager.getLoadedPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - responses: { - "200": { - description: "Successfully retrieved plugins", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-plugin", - }, - version: { - type: "string", - example: "1.0.0", - }, - status: { - type: "string", - example: "active", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving plugins", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting all registered plugins", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Returns current API configuration including data retention policies and security settings", + responses: { + "200": { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fetching_interval: { + type: "number", + example: 5, + }, + keep_data_for: { + type: "number", + example: 7, + }, + api_key: { + type: "string", + example: "hashed_api_key", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/plugins", + ({ set }) => { + try { + return pluginManager.getLoadedPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + responses: { + "200": { + description: "Successfully retrieved plugins", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-plugin", + }, + version: { + type: "string", + example: "1.0.0", + }, + status: { + type: "string", + example: "active", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving plugins", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting all registered plugins", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { fetching_interval, keep_data_for, api_key } = body; - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key) - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - responses: { - "200": { - description: "Successfully updated configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated DockStatAPI config", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error updating the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - } - ) - .get( - "/package", - async () => { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key), + ); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", + responses: { + "200": { + description: "Successfully updated configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated DockStatAPI config", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error updating the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), + }, + ) + .get( + "/package", + async () => { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json` - ); + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, + ); - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - responses: { - "200": { - description: "Successfully retrieved package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error while reading package.json", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .post( - "/backup", - async ({ set }) => { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return responseHandler.ok(set, backupFilename); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Backs up the internal database", - responses: { - "200": { - description: "Successfully created backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "backup_2024-03-20_12-00-00.db.bak", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error creating backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error backing up", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/backup", - async ({ set }) => { - try { - const backupFiles = readdirSync(backupDir); + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Displays package metadata including dependencies, contributors, and licensing information", + responses: { + "200": { + description: "Successfully retrieved package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0", + }, + description: { + type: "string", + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", + }, + license: { + type: "string", + example: "CC BY-NC 4.0", + }, + authorName: { + type: "string", + example: "ItsNik", + }, + authorEmail: { + type: "string", + example: "info@itsnik.de", + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik", + }, + contributors: { + type: "array", + items: { + type: "string", + }, + example: [], + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + }, + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error while reading package.json", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .post( + "/backup", + async ({ set }) => { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return responseHandler.ok(set, backupFilename); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Backs up the internal database", + responses: { + "200": { + description: "Successfully created backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "backup_2024-03-20_12-00-00.db.bak", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error creating backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error backing up", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/backup", + async ({ set }) => { + try { + const backupFiles = readdirSync(backupDir); - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Lists all available backups", - responses: { - "200": { - description: "Successfully retrieved backup list", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "string", - }, - example: [ - "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak", - ], - }, - }, - }, - }, - "400": { - description: "Error retrieving backup list", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Reading Backup directory", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Lists all available backups", + responses: { + "200": { + description: "Successfully retrieved backup list", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "string", + }, + example: [ + "backup_2024-03-20_12-00-00.db.bak", + "backup_2024-03-19_12-00-00.db.bak", + ], + }, + }, + }, + }, + "400": { + description: "Error retrieving backup list", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Reading Backup directory", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) - .get( - "/backup/download", - async ({ query, set }) => { - try { - const filename = query.filename || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; + .get( + "/backup/download", + async ({ query, set }) => { + try { + const filename = query.filename || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } - set.headers["Content-Type"] = "application/octet-stream"; - set.headers[ - "Content-Disposition" - ] = `attachment; filename="${filename}"`; - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Download a specific backup or the latest if no filename is provided", - responses: { - "200": { - description: "Successfully downloaded backup file", - content: { - "application/octet-stream": { - schema: { - type: "string", - format: "binary", - example: "Binary backup file content", - }, - }, - }, - headers: { - "Content-Disposition": { - schema: { - type: "string", - example: - 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', - }, - }, - }, - }, - "400": { - description: "Error downloading backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Backup download failed", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - filename: t.Optional(t.String()), - }), - } - ) - .post( - "/restore", - async ({ body, set }) => { - try { - const { file } = body; + set.headers["Content-Type"] = "application/octet-stream"; + set.headers["Content-Disposition"] = + `attachment; filename="${filename}"`; + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Download a specific backup or the latest if no filename is provided", + responses: { + "200": { + description: "Successfully downloaded backup file", + content: { + "application/octet-stream": { + schema: { + type: "string", + format: "binary", + example: "Binary backup file content", + }, + }, + }, + headers: { + "Content-Disposition": { + schema: { + type: "string", + example: + 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', + }, + }, + }, + }, + "400": { + description: "Error downloading backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Backup download failed", + }, + }, + }, + }, + }, + }, + }, + }, + query: t.Object({ + filename: t.Optional(t.String()), + }), + }, + ) + .post( + "/restore", + async ({ body, set }) => { + try { + const { file } = body; - set.headers["Content-Type"] = "text/html"; + set.headers["Content-Type"] = "text/html"; - if (!file) { - throw new Error("No file uploaded"); - } + if (!file) { + throw new Error("No file uploaded"); + } - if (!file.name.endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } + if (!file.name.endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); - return responseHandler.ok(set, "Database restored successfully"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - body: t.Object({ file: t.File() }), - detail: { - tags: ["Management"], - description: "Restore database from uploaded backup file", - responses: { - "200": { - description: "Successfully restored database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Database restored successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restoring database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Database restoration error", - }, - }, - }, - }, - }, - }, - }, - }, - } - ); + return responseHandler.ok(set, "Database restored successfully"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + body: t.Object({ file: t.File() }), + detail: { + tags: ["Management"], + description: "Restore database from uploaded backup file", + responses: { + "200": { + description: "Successfully restored database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Database restored successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error restoring database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Database restoration error", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts index 279fa2ba..fcd877e9 100644 --- a/src/routes/docker-manager.ts +++ b/src/routes/docker-manager.ts @@ -1,255 +1,255 @@ import { Elysia, t } from "elysia"; -import type { DockerHost } from "~/typings/docker"; import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; +import type { DockerHost } from "~/typings/docker"; export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) - .post( - "/add-host", - async ({ set, body }) => { - try { - dbFunctions.addDockerHost(body as DockerHost); - return responseHandler.ok(set, `Added docker host (${body.name})`); - } catch (error: unknown) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Registers a new Docker host to the monitoring system with connection details", - responses: { - "200": { - description: "Successfully added Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Added docker host (Localhost)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error adding Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error adding docker Host", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - name: t.String(), - hostAddress: t.String(), - secure: t.Boolean(), - }), - } - ) + .post( + "/add-host", + async ({ set, body }) => { + try { + dbFunctions.addDockerHost(body as DockerHost); + return responseHandler.ok(set, `Added docker host (${body.name})`); + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Registers a new Docker host to the monitoring system with connection details", + responses: { + "200": { + description: "Successfully added Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Added docker host (Localhost)", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error adding Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error adding docker Host", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + name: t.String(), + hostAddress: t.String(), + secure: t.Boolean(), + }), + }, + ) - .post( - "/update-host", - async ({ set, body }) => { - try { - set.status = 200; - dbFunctions.updateDockerHost(body); - return responseHandler.ok(set, `Updated docker host (${body.id})`); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies existing Docker host configuration parameters (name, address, security)", - responses: { - "200": { - description: "Successfully updated Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated docker host (1)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to update host", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - id: t.Number(), - name: t.String(), - hostAddress: t.String(), - secure: t.Boolean(), - }), - } - ) + .post( + "/update-host", + async ({ set, body }) => { + try { + set.status = 200; + dbFunctions.updateDockerHost(body); + return responseHandler.ok(set, `Updated docker host (${body.id})`); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies existing Docker host configuration parameters (name, address, security)", + responses: { + "200": { + description: "Successfully updated Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated docker host (1)", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to update host", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + id: t.Number(), + name: t.String(), + hostAddress: t.String(), + secure: t.Boolean(), + }), + }, + ) - .get( - "/hosts", - async ({ set }) => { - try { - const dockerHosts = dbFunctions.getDockerHosts(); + .get( + "/hosts", + async ({ set }) => { + try { + const dockerHosts = dbFunctions.getDockerHosts(); - logger.debug("Retrieved docker hosts"); - return dockerHosts; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all configured Docker hosts with their connection settings", - responses: { - "200": { - description: "Successfully retrieved Docker hosts", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "number", - example: 1, - }, - name: { - type: "string", - example: "Localhost", - }, - hostAddress: { - type: "string", - example: "localhost:2375", - }, - secure: { - type: "boolean", - example: false, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving Docker hosts", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve hosts", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) + logger.debug("Retrieved docker hosts"); + return dockerHosts; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all configured Docker hosts with their connection settings", + responses: { + "200": { + description: "Successfully retrieved Docker hosts", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "number", + example: 1, + }, + name: { + type: "string", + example: "Localhost", + }, + hostAddress: { + type: "string", + example: "localhost:2375", + }, + secure: { + type: "boolean", + example: false, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving Docker hosts", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve hosts", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) - .delete( - "/hosts/:id", - async ({ set, params }) => { - try { - set.status = 200; - dbFunctions.deleteDockerHost(params.id); - return responseHandler.ok(set, `Deleted docker host (${params.id})`); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Removes Docker host from monitoring system and clears associated data", - responses: { - "200": { - description: "Successfully deleted Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Deleted docker host (1)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error deleting Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to delete host", - }, - }, - }, - }, - }, - }, - }, - }, - params: t.Object({ - id: t.Number(), - }), - } - ); + .delete( + "/hosts/:id", + async ({ set, params }) => { + try { + set.status = 200; + dbFunctions.deleteDockerHost(params.id); + return responseHandler.ok(set, `Deleted docker host (${params.id})`); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Removes Docker host from monitoring system and clears associated data", + responses: { + "200": { + description: "Successfully deleted Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Deleted docker host (1)", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error deleting Docker host", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to delete host", + }, + }, + }, + }, + }, + }, + }, + }, + params: t.Object({ + id: t.Number(), + }), + }, + ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index f9d966d6..aa968d2d 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -3,8 +3,8 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; @@ -13,586 +13,586 @@ import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get( - "/containers", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; + .get( + "/containers", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - return responseHandler.error( - set, - pingError as string, - "Docker host connection failed" - ); - } + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + return responseHandler.error( + set, + pingError as string, + "Docker host connection failed", + ); + } - const hostContainers = await docker.listContainers({ all: true }); + const hostContainers = await docker.listContainers({ all: true }); - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - return responseHandler.reject( - set, - reject, - "An error occurred", - error - ); - } - if (!stats) { - return responseHandler.reject( - set, - reject, - "No stats available" - ); - } - resolve(stats); - }); - } - ); + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return responseHandler.reject( + set, + reject, + "An error occurred", + error, + ); + } + if (!stats) { + return responseHandler.reject( + set, + reject, + "No stats available", + ); + } + resolve(stats); + }); + }, + ); - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError - ); - } - }) - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }) - ); + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }), + ); - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", - responses: { - "200": { - description: "Successfully retrieved container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - containers: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "string", - example: "abc123def456", - }, - hostId: { - type: "string", - example: "1", - }, - name: { - type: "string", - example: "example-container", - }, - image: { - type: "string", - example: "nginx:latest", - }, - status: { - type: "string", - example: "running", - }, - state: { - type: "string", - example: "running", - }, - cpuUsage: { - type: "number", - example: 0.5, - }, - memoryUsage: { - type: "number", - example: 1024, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve containers", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", + responses: { + "200": { + description: "Successfully retrieved container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + containers: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + example: "abc123def456", + }, + hostId: { + type: "string", + example: "1", + }, + name: { + type: "string", + example: "example-container", + }, + image: { + type: "string", + example: "nginx:latest", + }, + status: { + type: "string", + example: "running", + }, + state: { + type: "string", + example: "running", + }, + cpuUsage: { + type: "number", + example: 0.5, + }, + memoryUsage: { + type: "number", + example: 1024, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve containers", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - logger.debug("Fetched all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - logger.debug("Fetched stats for all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for all hosts", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/hosts/:id", - async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched stats for all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for all hosts", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts/:id", + async ({ params, set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = findObjectByKey(hosts, "id", Number(params.id)); - if (!host) { - return responseHandler.simple_error( - set, - `Host (${params.id}) not found` - ); - } + const host = findObjectByKey(hosts, "id", Number(params.id)); + if (!host) { + return responseHandler.simple_error( + set, + `Host (${params.id}) not found`, + ); + } - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ); + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ); diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts index 64e04d22..e1b52d7f 100644 --- a/src/tests/docker-manager.spec.ts +++ b/src/tests/docker-manager.spec.ts @@ -3,462 +3,462 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { dockerRoutes } from "~/routes/docker-manager"; import { - generateMarkdownReport, - recordTestResult, - testResults, + generateMarkdownReport, + recordTestResult, + testResults, } from "./markdown-exporter"; import type { TestContext } from "./markdown-exporter"; type DockerHost = { - id?: number; - name: string; - hostAddress: string; - secure: boolean; + id?: number; + name: string; + hostAddress: string; + secure: boolean; }; const mockDb = { - addDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - updateDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - getDockerHosts: mock(() => []), - deleteDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), + addDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + updateDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), + getDockerHosts: mock(() => []), + deleteDockerHost: mock(() => ({ + changes: 1, + lastInsertRowid: 1, + })), }; mock.module("~/core/database", () => ({ - dbFunctions: mockDb, + dbFunctions: mockDb, })); mock.module("~/core/utils/logger", () => ({ - logger: { - debug: mock(), - info: mock(), - error: mock(), - }, + logger: { + debug: mock(), + info: mock(), + error: mock(), + }, })); const createApp = () => new Elysia().use(dockerRoutes).decorate({}); async function captureTestContext( - req: Request, - res: Response + req: Request, + res: Response, ): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: unknown; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; + const responseStatus = res.status; + const responseHeaders = Object.fromEntries(res.headers.entries()); + let responseBody: unknown; + + try { + responseBody = await res.clone().json(); + } catch (parseError) { + try { + responseBody = await res.clone().text(); + } catch { + responseBody = "Unparseable response content"; + } + } + + return { + request: { + method: req.method, + url: req.url, + headers: Object.fromEntries(req.headers.entries()), + body: req.body ? await req.clone().text() : undefined, + }, + response: { + status: responseStatus, + headers: responseHeaders, + body: responseBody, + }, + }; } describe("Docker Configuration Endpoints", () => { - beforeEach(() => { - mockDb.addDockerHost.mockClear(); - mockDb.updateDockerHost.mockClear(); - mockDb.getDockerHosts.mockClear(); - mockDb.deleteDockerHost.mockClear(); - }); - - describe("POST /docker-config/add-host", () => { - it("should add a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host1", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Added docker host (${host.name})`, - }); - expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with success message", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when adding a docker host fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host2", - hostAddress: "invalid", - secure: true, - }; - - // Set mock implementation - mockDb.addDockerHost.mockImplementationOnce(() => { - throw new Error("DB error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), - }); - - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error structure", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("POST /docker-config/update-host", () => { - it("should update a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 1, - name: "Host1-upd", - hostAddress: "127.0.0.1:2376", - secure: true, - }; - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Updated docker host (${host.id})`, - }); - expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when update fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 2, - name: "Host2", - hostAddress: "x", - secure: false, - }; - - mockDb.updateDockerHost.mockImplementationOnce(() => { - throw new Error("Update error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), - }); - - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("GET /docker-config/hosts", () => { - it("should retrieve list of hosts", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const hosts: DockerHost[] = [ - { id: 1, name: "H1", hostAddress: "a", secure: false }, - ]; - - mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual(hosts); - - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with hosts array", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when retrieval fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - mockDb.getDockerHosts.mockImplementationOnce(() => { - throw new Error("Fetch error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), - }); - - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("DELETE /docker-config/hosts/:id", () => { - it("should delete a host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 5; - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Deleted docker host (${id})`, - }); - expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); - - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with deletion confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when delete fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 6; - - mockDb.deleteDockerHost.mockImplementationOnce(() => { - throw new Error("Delete error"); - }); - - try { - const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), - }); - - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); + beforeEach(() => { + mockDb.addDockerHost.mockClear(); + mockDb.updateDockerHost.mockClear(); + mockDb.getDockerHosts.mockClear(); + mockDb.deleteDockerHost.mockClear(); + }); + + describe("POST /docker-config/add-host", () => { + it("should add a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host1", + hostAddress: "127.0.0.1:2375", + secure: false, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Added docker host (${host.name})`, + }); + expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host success", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with success message", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when adding a docker host fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + name: "Host2", + hostAddress: "invalid", + secure: true, + }; + + // Set mock implementation + mockDb.addDockerHost.mockImplementationOnce(() => { + throw new Error("DB error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/add-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "add-host failure", + suite: "Docker Config - Add Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error structure", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("POST /docker-config/update-host", () => { + it("should update a docker host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 1, + name: "Host1-upd", + hostAddress: "127.0.0.1:2376", + secure: true, + }; + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Updated docker host (${host.id})`, + }); + expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); + + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host success", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with update confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when update fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const host: DockerHost = { + id: 2, + name: "Host2", + hostAddress: "x", + secure: false, + }; + + mockDb.updateDockerHost.mockImplementationOnce(() => { + throw new Error("Update error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/update-host", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "update-host failure", + suite: "Docker Config - Update Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("GET /docker-config/hosts", () => { + it("should retrieve list of hosts", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const hosts: DockerHost[] = [ + { id: 1, name: "H1", hostAddress: "a", secure: false }, + ]; + + mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toEqual(hosts); + + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts success", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with hosts array", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when retrieval fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + + mockDb.getDockerHosts.mockImplementationOnce(() => { + throw new Error("Fetch error"); + }); + + try { + const app = createApp(); + const req = new Request("http://localhost/docker-config/hosts"); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "get-hosts failure", + suite: "Docker Config - List Hosts", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); + + describe("DELETE /docker-config/hosts/:id", () => { + it("should delete a host successfully", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 5; + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(200); + expect(context.response.body).toMatchObject({ + message: `Deleted docker host (${id})`, + }); + expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); + + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host success", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "200 OK with deletion confirmation", + received: context?.response, + }, + }); + throw error; + } + }); + + it("should handle error when delete fails", async () => { + const start = Date.now(); + let context: TestContext | undefined; + const id = 6; + + mockDb.deleteDockerHost.mockImplementationOnce(() => { + throw new Error("Delete error"); + }); + + try { + const app = createApp(); + const req = new Request(`http://localhost/docker-config/hosts/${id}`, { + method: "DELETE", + }); + const res = await app.handle(req); + context = await captureTestContext(req, res); + + expect(res.status).toBe(500); + expect(context.response.body).toMatchObject({ + message: expect.any(String), + }); + + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + context, + }); + } catch (error) { + recordTestResult({ + name: "delete-host failure", + suite: "Docker Config - Delete Host", + time: Date.now() - start, + error: error as Error, + context, + errorDetails: { + expected: "400 Error with error details", + received: context?.response, + }, + }); + throw error; + } + }); + }); }); afterAll(() => { - generateMarkdownReport(); + generateMarkdownReport(); }); From 2e8528ab8a500a925f3411582a21fbeccc683262 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 16 May 2025 10:45:58 +0200 Subject: [PATCH 324/369] UT: Fix wrong body selection --- src/tests/docker-manager.spec.ts | 92 +++++++++++++++++++------------- 1 file changed, 55 insertions(+), 37 deletions(-) diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts index 64e04d22..3c643506 100644 --- a/src/tests/docker-manager.spec.ts +++ b/src/tests/docker-manager.spec.ts @@ -99,11 +99,14 @@ describe("Docker Configuration Endpoints", () => { try { const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); + const req = new Request( + "http://localhost:3000/docker-config/add-host", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + } + ); const res = await app.handle(req); context = await captureTestContext(req, res); @@ -146,22 +149,25 @@ describe("Docker Configuration Endpoints", () => { // Set mock implementation mockDb.addDockerHost.mockImplementationOnce(() => { - throw new Error("DB error"); + throw new Error("Mock Database Error"); }); try { const app = createApp(); - const req = new Request("http://localhost/docker-config/add-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); + const req = new Request( + "http://localhost:3000/docker-config/add-host", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + } + ); const res = await app.handle(req); context = await captureTestContext(req, res); expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), + expect(context.response).toMatchObject({ + body: expect.any(String), }); recordTestResult({ @@ -200,11 +206,14 @@ describe("Docker Configuration Endpoints", () => { try { const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); + const req = new Request( + "http://localhost:3000/docker-config/update-host", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + } + ); const res = await app.handle(req); context = await captureTestContext(req, res); @@ -252,17 +261,20 @@ describe("Docker Configuration Endpoints", () => { try { const app = createApp(); - const req = new Request("http://localhost/docker-config/update-host", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }); + const req = new Request( + "http://localhost:3000/docker-config/update-host", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(host), + } + ); const res = await app.handle(req); context = await captureTestContext(req, res); expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), + expect(context.response).toMatchObject({ + body: expect.any(String), }); recordTestResult({ @@ -300,7 +312,7 @@ describe("Docker Configuration Endpoints", () => { try { const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); + const req = new Request("http://localhost:3000/docker-config/hosts"); const res = await app.handle(req); context = await captureTestContext(req, res); @@ -339,13 +351,13 @@ describe("Docker Configuration Endpoints", () => { try { const app = createApp(); - const req = new Request("http://localhost/docker-config/hosts"); + const req = new Request("http://localhost:3000/docker-config/hosts"); const res = await app.handle(req); context = await captureTestContext(req, res); expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), + expect(context.response).toMatchObject({ + body: expect.any(String), }); recordTestResult({ @@ -379,9 +391,12 @@ describe("Docker Configuration Endpoints", () => { try { const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); + const req = new Request( + `http://localhost:3000/docker-config/hosts/${id}`, + { + method: "DELETE", + } + ); const res = await app.handle(req); context = await captureTestContext(req, res); @@ -424,15 +439,18 @@ describe("Docker Configuration Endpoints", () => { try { const app = createApp(); - const req = new Request(`http://localhost/docker-config/hosts/${id}`, { - method: "DELETE", - }); + const req = new Request( + `http://localhost:3000/docker-config/hosts/${id}`, + { + method: "DELETE", + } + ); const res = await app.handle(req); context = await captureTestContext(req, res); expect(res.status).toBe(500); - expect(context.response.body).toMatchObject({ - message: expect.any(String), + expect(context.response).toMatchObject({ + body: expect.any(String), }); recordTestResult({ From 3a9a3dc8d37c0eea1b7af67b18176b8209989dcb Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Mon, 2 Jun 2025 08:26:24 +0200 Subject: [PATCH 325/369] Feat: I don't even know what has changed, but a lot ig --- .github/workflows/ci.yml | 3 +- docker/docker-compose.unit-test.yaml | 42 + knip.report.md | Bin 36 -> 0 bytes package.json | 12 +- src/core/database/stacks.ts | 68 +- src/core/plugins/plugin-manager.ts | 249 +++--- src/core/stacks/controller.ts | 698 ++++++++-------- src/core/utils/helpers.ts | 25 +- src/plugins/example.plugin.ts | 175 ++-- src/plugins/telegram.plugin.ts | 48 +- src/routes/api-config.ts | 1113 ++++++++++++------------- src/routes/docker-websocket.ts | 214 ++--- src/routes/live-logs.ts | 48 +- src/routes/live-stacks.ts | 36 +- src/routes/stacks.ts | 1152 +++++++++++++------------- src/typings | 2 +- 16 files changed, 1991 insertions(+), 1894 deletions(-) create mode 100644 docker/docker-compose.unit-test.yaml delete mode 100644 knip.report.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cf46283..87552540 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - name: Run unit tests run: | export PAD_NEW_LINES=false - docker compose -f docker/docker-compose.dev.yaml up -d + docker compose -f docker/docker-compose.unit-test.yaml up -d bun test - name: Log unit test files @@ -44,6 +44,7 @@ jobs: SUMMARY="$(echo -e "${SUMMARY}\n$(cat "reports/markdown/${element}")")" done echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY + build-scan: name: Build and Security Scan runs-on: ubuntu-latest diff --git a/docker/docker-compose.unit-test.yaml b/docker/docker-compose.unit-test.yaml new file mode 100644 index 00000000..0dc445e3 --- /dev/null +++ b/docker/docker-compose.unit-test.yaml @@ -0,0 +1,42 @@ +name: "dockstatapi-unit-test" +services: + socket-proxy: + container_name: socket-proxy + image: lscr.io/linuxserver/socket-proxy:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + restart: unless-stopped + read_only: true + tmpfs: + - /run + ports: + - 2375:2375 + environment: + - ALLOW_START=1 + - ALLOW_STOP=1 + - ALLOW_RESTARTS=1 + - AUTH=1 + - BUILD=1 + - COMMIT=1 + - CONFIGS=1 + - CONTAINERS=1 + - DISABLE_IPV6=1 + - DISTRIBUTION=1 + - EVENTS=1 + - EXEC=1 + - IMAGES=1 + - INFO=1 + - NETWORKS=1 + - NODES=1 + - PING=1 + - PLUGINS=1 + - POST=1 + - PROXY_READ_TIMEOUT=240 + - SECRETS=1 + - SERVICES=1 + - SESSION=1 + - SWARM=1 + - SYSTEM=1 + - TASKS=1 + - VERSION=1 + - VOLUMES=1 diff --git a/knip.report.md b/knip.report.md deleted file mode 100644 index 883973e8cd66b2063469f101056c1e31272ec606..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36 jcmezWPnki1!J8qEA(Np1$SPt;1=9IIx`ct3feVZQqx=TF diff --git a/package.json b/package.json index c4e322e6..5b696e96 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "build:prod": "NODE_ENV=production bun build --no-native --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts", "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", "clean": "bun run clean:win || bun run clean:lin", - "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi* && cmd /c del /Q reports/markdown/*.md && echo 'success'", - "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi* && rm -f reports/markdown/*.md && echo 'success'", + "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi* && cmd /c del /Q stacks/* && cmd /c del /Q reports/markdown/*.md && echo 'success'", + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi* && rm -rf stacks/* && rm -f reports/markdown/*.md && echo 'success'", "knip": "knip", "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src" }, @@ -32,18 +32,18 @@ "docker-compose": "^1.2.0", "dockerode": "^4.0.6", "elysia": "latest", - "elysia-remote-dts": "^1.0.2", + "elysia-remote-dts": "^1.0.3", "knip": "latest", "logestic": "^1.2.4", "split2": "^4.2.0", "winston": "^3.17.0", - "yaml": "^2.7.1" + "yaml": "^2.8.0" }, "devDependencies": { "@biomejs/biome": "1.9.4", "@types/bun": "latest", - "@types/dockerode": "^3.3.38", - "@types/node": "^22.15.17", + "@types/dockerode": "^3.3.39", + "@types/node": "^22.15.29", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", diff --git a/src/core/database/stacks.ts b/src/core/database/stacks.ts index 1c81e6d9..a442482b 100644 --- a/src/core/database/stacks.ts +++ b/src/core/database/stacks.ts @@ -5,62 +5,62 @@ import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare(` + insert: db.prepare(` INSERT INTO stacks_config ( name, version, custom, source, compose_spec ) VALUES (?, ?, ?, ?, ?) `), - selectAll: db.prepare(` + selectAll: db.prepare(` SELECT id, name, version, custom, source, compose_spec FROM stacks_config ORDER BY name DESC `), - update: db.prepare(` - UPDATE stacks_config + update: db.prepare(` + UPDATE stacks_config SET name = ?, custom = ?, source = ?, compose_spec = ? WHERE name = ? `), - delete: db.prepare("DELETE FROM stacks_config WHERE id = ?"), + delete: db.prepare("DELETE FROM stacks_config WHERE id = ?"), }; export function addStack(stack: stacks_config) { - executeDbOperation("Add Stack", () => - stmt.insert.run( - stack.name, - stack.version, - stack.custom, - stack.source, - stack.compose_spec, - ), - ); + executeDbOperation("Add Stack", () => + stmt.insert.run( + stack.name, + stack.version, + stack.custom, + stack.source, + stack.compose_spec + ) + ); - return findObjectByKey(getStacks(), "name", stack.name)?.id; + return findObjectByKey(getStacks(), "name", stack.name)?.id; } export function getStacks() { - return executeDbOperation("Get Stacks", () => - stmt.selectAll.all(), - ) as Stack[]; + return executeDbOperation("Get Stacks", () => + stmt.selectAll.all() + ) as Stack[]; } export function deleteStack(id: number) { - return executeDbOperation( - "Delete Stack", - () => stmt.delete.run(id), - () => { - if (typeof id !== "number") throw new TypeError("Invalid stack ID"); - }, - ); + return executeDbOperation( + "Delete Stack", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid stack ID"); + } + ); } export function updateStack(stack: stacks_config) { - return executeDbOperation("Update Stack", () => - stmt.update.run( - stack.version, - stack.custom, - stack.source, - stack.name, - stack.compose_spec, - ), - ); + return executeDbOperation("Update Stack", () => + stmt.update.run( + stack.version, + stack.custom, + stack.source, + stack.name, + stack.compose_spec + ) + ); } diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index 83d623f9..ad9bfeee 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -1,120 +1,145 @@ import { EventEmitter } from "node:events"; import type { ContainerInfo } from "~/typings/docker"; -import type { Plugin } from "~/typings/plugin"; +import type { Plugin, Hooks, PluginInfo } from "~/typings/plugin"; import { logger } from "../utils/logger"; class PluginManager extends EventEmitter { - private plugins: Map = new Map(); - - register(plugin: Plugin) { - try { - this.plugins.set(plugin.name, plugin); - logger.debug(`Registered plugin: ${plugin.name}`); - } catch (error) { - logger.error( - `Registering plugin ${plugin.name} failed: ${error as string}`, - ); - } - } - - unregister(name: string) { - this.plugins.delete(name); - } - - getLoadedPlugins(): string[] { - return Array.from(this.plugins.keys()); - } - - // Trigger plugin flows: - handleContainerStop(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerStop?.(containerInfo); - } - } - - handleContainerStart(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerStart?.(containerInfo); - } - } - - handleContainerExit(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerExit?.(containerInfo); - } - } - - handleContainerCreate(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerCreate?.(containerInfo); - } - } - - handleContainerDestroy(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerDestroy?.(containerInfo); - } - } - - handleContainerPause(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerPause?.(containerInfo); - } - } - - handleContainerUnpause(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerUnpause?.(containerInfo); - } - } - - handleContainerRestart(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerRestart?.(containerInfo); - } - } - - handleContainerUpdate(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerUpdate?.(containerInfo); - } - } - - handleContainerRename(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerRename?.(containerInfo); - } - } - - handleContainerHealthStatus(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerHealthStatus?.(containerInfo); - } - } - - handleHostUnreachable(host: string, err: string) { - for (const [, plugin] of this.plugins) { - plugin.onHostUnreachable?.(host, err); - } - } - - handleHostReachableAgain(host: string) { - for (const [, plugin] of this.plugins) { - plugin.onHostReachableAgain?.(host); - } - } - - handleContainerKill(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerKill?.(containerInfo); - } - } - - handleContainerDie(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.handleContainerDie?.(containerInfo); - } - } + private plugins: Map = new Map(); + + register(plugin: Plugin) { + try { + this.plugins.set(plugin.name, plugin); + logger.debug(`Registered plugin: ${plugin.name}`); + } catch (error) { + logger.error( + `Registering plugin ${plugin.name} failed: ${error as string}` + ); + } + } + + unregister(name: string) { + this.plugins.delete(name); + } + + getLoadedPlugins(): PluginInfo[] { + return Array.from(this.plugins.values()).map((plugin) => { + const hooks: Hooks = { + onContainerStart: !!plugin.onContainerStart, + onContainerStop: !!plugin.onContainerStop, + onContainerExit: !!plugin.onContainerExit, + onContainerCreate: !!plugin.onContainerCreate, + onContainerKill: !!plugin.onContainerKill, + handleContainerDie: !!plugin.handleContainerDie, + onContainerDestroy: !!plugin.onContainerDestroy, + onContainerPause: !!plugin.onContainerPause, + onContainerUnpause: !!plugin.onContainerUnpause, + onContainerRestart: !!plugin.onContainerRestart, + onContainerUpdate: !!plugin.onContainerUpdate, + onContainerRename: !!plugin.onContainerRename, + onContainerHealthStatus: !!plugin.onContainerHealthStatus, + onHostUnreachable: !!plugin.onHostUnreachable, + onHostReachableAgain: !!plugin.onHostReachableAgain, + }; + + return { + name: plugin.name, + version: plugin.version, + status: "active", + usedHooks: hooks, + }; + }); + } + + // Trigger plugin flows: + handleContainerStop(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStop?.(containerInfo); + } + } + + handleContainerStart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStart?.(containerInfo); + } + } + + handleContainerExit(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerExit?.(containerInfo); + } + } + + handleContainerCreate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerCreate?.(containerInfo); + } + } + + handleContainerDestroy(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerDestroy?.(containerInfo); + } + } + + handleContainerPause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerPause?.(containerInfo); + } + } + + handleContainerUnpause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUnpause?.(containerInfo); + } + } + + handleContainerRestart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRestart?.(containerInfo); + } + } + + handleContainerUpdate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUpdate?.(containerInfo); + } + } + + handleContainerRename(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRename?.(containerInfo); + } + } + + handleContainerHealthStatus(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerHealthStatus?.(containerInfo); + } + } + + handleHostUnreachable(host: string, err: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostUnreachable?.(host, err); + } + } + + handleHostReachableAgain(host: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostReachableAgain?.(host); + } + } + + handleContainerKill(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerKill?.(containerInfo); + } + } + + handleContainerDie(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.handleContainerDie?.(containerInfo); + } + } } export const pluginManager = new PluginManager(); diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 1f506bf8..60047f8a 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -9,393 +9,409 @@ import type { ComposeSpec, Stack } from "~/typings/docker-compose"; import { findObjectByKey } from "../utils/helpers"; const wrapProgressCallback = (progressCallback?: (log: string) => void) => { - return progressCallback - ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { - const log = chunk.toString(); - progressCallback(log); - } - : undefined; + return progressCallback + ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; }; async function getStackName(stack_id: number): Promise { - logger.debug(`Fetching stack name for id ${stack_id}`); - const stacks = dbFunctions.getStacks(); - const stack = findObjectByKey(stacks, "id", stack_id); - if (!stack) { - throw new Error(`Stack with id ${stack_id} not found`); - } - return stack.name; + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = findObjectByKey(stacks, "id", stack_id); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; } async function runStackCommand( - stack_id: number, - command: ( - cwd: string, - progressCallback?: (log: string) => void, - ) => Promise, - action: string, + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void + ) => Promise, + action: string ): Promise { - try { - logger.debug( - `Starting runStackCommand for stack_id=${stack_id}, action="${action}"`, - ); - - const stackName = await getStackName(stack_id); - logger.debug( - `Retrieved stack name "${stackName}" for stack_id=${stack_id}`, - ); - - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); - - const progressCallback = (log: string) => { - const message = log.trim(); - logger.debug( - `Progress for stack_id=${stack_id}, action="${action}": ${message}`, - ); - - // ERROR HANDLING FOR COMPOSE ACTIONS - if (message.includes("Error response from daemon")) { - logger.error( - `Error response from daemon: ${ - message.split("Error response from daemon:")[1] - }`, - ); - } - - postToClient({ - type: "stack-progress", - data: { - stack_id, - action, - message, - timestamp: new Date().toISOString(), - }, - }); - }; - - logger.debug( - `Executing command for stack_id=${stack_id}, action="${action}"`, - ); - const result = await command(stackPath, progressCallback); - logger.debug( - `Successfully completed command for stack_id=${stack_id}, action="${action}"`, - ); - - return result; - } catch (error) { - logger.debug( - `Error occurred for stack_id=${stack_id}, action="${action}": ${String( - error, - )}`, - ); - postToClient({ - type: "stack-error", - data: { - stack_id, - action, - message: String(error), - timestamp: new Date().toISOString(), - }, - }); - throw new Error( - `Error while ${action} stack "${stack_id}": ${String(error)}`, - ); - } + try { + logger.debug( + `Starting runStackCommand for stack_id=${stack_id}, action="${action}"` + ); + + const stackName = await getStackName(stack_id); + logger.debug( + `Retrieved stack name "${stackName}" for stack_id=${stack_id}` + ); + + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); + + const progressCallback = (log: string) => { + const message = log.trim(); + logger.debug( + `Progress for stack_id=${stack_id}, action="${action}": ${message}` + ); + + // ERROR HANDLING FOR COMPOSE ACTIONS + if (message.includes("Error response from daemon")) { + logger.error( + `Error response from daemon: ${ + message.split("Error response from daemon:")[1] + }` + ); + } + + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message, + timestamp: new Date().toISOString(), + }, + }); + }; + + logger.debug( + `Executing command for stack_id=${stack_id}, action="${action}"` + ); + const result = await command(stackPath, progressCallback); + logger.debug( + `Successfully completed command for stack_id=${stack_id}, action="${action}"` + ); + + return result; + } catch (error) { + logger.debug( + `Error occurred for stack_id=${stack_id}, action="${action}": ${String( + error + )}` + ); + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: String(error), + timestamp: new Date().toISOString(), + }, + }); + throw new Error( + `Error while ${action} stack "${stack_id}": ${String(error)}` + ); + } } async function getStackPath(stack: Stack): Promise { - const stackName = stack.name.trim().replace(/\s+/g, "_"); - const stackId = stack.id; + const stackName = stack.name.trim().replace(/\s+/g, "_"); + const stackId = stack.id; - if (!stackId) { - logger.error("Stack could not be parsed"); - throw new Error("Stack could not be parsed"); - } + if (!stackId) { + logger.error("Stack could not be parsed"); + throw new Error("Stack could not be parsed"); + } - return `stacks/${stackId}-${stackName}`; + return `stacks/${stackId}-${stackName}`; } async function createStackYAML(compose_spec: Stack): Promise { - const yaml = YAML.stringify(compose_spec.compose_spec); - const stackPath = await getStackPath(compose_spec); - await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { - createPath: true, - }); + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); } export async function deployStack(stack_config: stacks_config): Promise { - try { - logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); - - if (!stack_config.name) { - throw new Error("Stack name needed"); - } - - const jsonStringStack = { - ...stack_config, - compose_spec: JSON.stringify(stack_config.compose_spec), - }; - - const stackId = dbFunctions.addStack(jsonStringStack); - - if (!stackId) { - throw new Error("Failed to add stack to database"); - } - - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "pending", - message: "Creating stack configuration", - }, - }); - - const stackYaml: Stack = { - id: stackId, - name: stack_config.name, - source: stack_config.source, - version: stack_config.version, - compose_spec: stack_config.compose_spec as unknown as ComposeSpec, // Weird stuff i am doing here... smh - }; - - await createStackYAML(stackYaml); - - await runStackCommand( - stackId, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "deploying", - ); - - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "deployed", - message: "Stack deployed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id: 0, - action: "deploying", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); + + if (!stack_config.name) { + throw new Error("Stack name needed"); + } + + const jsonStringStack = { + ...stack_config, + compose_spec: JSON.stringify(stack_config.compose_spec), + }; + + const stackId = dbFunctions.addStack(jsonStringStack); + + if (!stackId) { + throw new Error("Failed to add stack to database"); + } + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }); + + const stackYaml: Stack = { + id: stackId, + name: stack_config.name, + source: stack_config.source, + version: stack_config.version, + compose_spec: stack_config.compose_spec as unknown as ComposeSpec, // Weird stuff i am doing here... smh + }; + + await createStackYAML(stackYaml); + + await runStackCommand( + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying" + ); + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id: 0, + action: "deploying", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } export async function stopStack(stack_id: number): Promise { - // Note the await to discard the result (convert to void) - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.downAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "stopping", - ); + // Note the await to discard the result (convert to void) + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.downAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping" + ); } export async function startStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "starting", - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting" + ); } export async function pullStackImages(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.pullAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "pulling-images", - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images" + ); } export async function restartStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.restartAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "restarting", - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting" + ); } export async function getStackStatus( - stack_id: number, - //biome-ignore lint/suspicious/noExplicitAny: + stack_id: number + //biome-ignore lint/suspicious/noExplicitAny: ): Promise> { - const status = await runStackCommand( - stack_id, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - //biome-ignore lint/suspicious/noExplicitAny: - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check", - ); - return status; + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check" + ); + return status; } export async function removeStack(stack_id: number): Promise { - try { - const _ = dbFunctions.deleteStack(stack_id); - - await runStackCommand( - stack_id, - async (cwd, progressCallback) => { - await DockerCompose.down({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }); - }, - "removing", - ); - - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - - try { - await rm(stackPath, { recursive: true }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } - - postToClient({ - type: "stack-removed", - data: { - stack_id, - message: "Stack removed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + try { + const _ = dbFunctions.deleteStack(stack_id); + + await runStackCommand( + stack_id, + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }); + }, + "removing" + ); + + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + + try { + await rm(stackPath, { recursive: true }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } + + postToClient({ + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } interface DockerServiceStatus { - status: string; - ports: string[]; + status: string; + ports: string[]; } interface StackStatus { - services: Record; - healthy: number; - unhealthy: number; - total: number; + services: Record; + healthy: number; + unhealthy: number; + total: number; } type StacksStatus = Record; export async function getAllStacksStatus(): Promise { - try { - const stacks = dbFunctions.getStacks(); - - const statusResults = await Promise.all( - stacks.map(async (stack) => { - const status = await runStackCommand( - stack.id as number, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - const services = rawStatus.data.services.reduce( - (acc: Record, service) => { - acc[service.name] = { - status: service.state, - ports: service.ports.map( - (port) => `${port.mapped?.address}:${port.mapped?.port}`, - ), - }; - return acc; - }, - {}, - ); - - const statusValues = Object.values(services); - return { - services, - healthy: statusValues.filter( - (s) => s.status === "running" || s.status.includes("Up"), - ).length, - unhealthy: statusValues.filter( - (s) => s.status !== "running" && !s.status.includes("Up"), - ).length, - total: statusValues.length, - }; - }, - "status-check", - ); - return { stackId: stack.id, status }; - }), - ); - - return statusResults.reduce((acc, { stackId, status }) => { - acc[String(stackId)] = status; - return acc; - }, {} as StacksStatus); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + try { + const stacks = dbFunctions.getStacks(); + + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.id as number, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + const services = rawStatus.data.services.reduce( + (acc: Record, service) => { + acc[service.name] = { + status: service.state, + ports: service.ports.map( + (port) => `${port.mapped?.address}:${port.mapped?.port}` + ), + }; + return acc; + }, + {} + ); + + const statusValues = Object.values(services); + return { + services, + healthy: statusValues.filter( + (s) => s.status === "running" || s.status.includes("Up") + ).length, + unhealthy: statusValues.filter( + (s) => s.status !== "running" && !s.status.includes("Up") + ).length, + total: statusValues.length, + }; + }, + "status-check" + ); + return { stackId: stack.id, status }; + }) + ); + + return statusResults.reduce((acc, { stackId, status }) => { + acc[String(stackId)] = status; + return acc; + }, {} as StacksStatus); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} + +async function backupStack(stackId: number) { + if (!stackId) { + throw new Error("No Stack ID provided"); + } + + const stacks = dbFunctions.getStacks(); + + const stack = findObjectByKey(stacks, "id", stackId); + + if (!stack) { + throw new Error(`No Stack with Id: ${stackId} found`); + } + + const stack_path = `${stack.id}-${stack.name}`; } diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts index 6c3e79e6..232f0244 100644 --- a/src/core/utils/helpers.ts +++ b/src/core/utils/helpers.ts @@ -1,13 +1,22 @@ import { logger } from "./logger"; +/** + * Finds and returns the first object in an array where the specified key matches the given value. + * + * @template T - The type of the objects in the array. + * @param {T[]} array - The array of objects to search through. + * @param {keyof T} key - The key of the object to match against. + * @param {T[keyof T]} value - The value to match the key against. + * @returns {T | undefined} The first matching object, or undefined if no match is found. + */ export function findObjectByKey( - array: T[], - key: keyof T, - value: T[keyof T], + array: T[], + key: keyof T, + value: T[keyof T] ): T | undefined { - logger.debug( - `Searching for key: ${String(key)} with value: ${String(value)}`, - ); - const data = array.find((item) => item[key] === value); - return data; + logger.debug( + `Searching for key: ${String(key)} with value: ${String(value)}` + ); + const data = array.find((item) => item[key] === value); + return data; } diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index 633eea41..824557ad 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -6,93 +6,94 @@ import type { Plugin } from "~/typings/plugin"; // See https://outline.itsnik.de/s/dockstat/doc/plugin-development-3UBj9gNMKF for more info const ExamplePlugin: Plugin = { - name: "Example Plugin", - - async onContainerStart(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} started on ${containerInfo.hostId}`, - ); - }, - - async onContainerStop(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} stopped on ${containerInfo.hostId}`, - ); - }, - - async onContainerExit(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} exited on ${containerInfo.hostId}`, - ); - }, - - async onContainerCreate(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} created on ${containerInfo.hostId}`, - ); - }, - - async onContainerDestroy(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}`, - ); - }, - - async onContainerPause(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} pause on ${containerInfo.hostId}`, - ); - }, - - async onContainerUnpause(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} resumed on ${containerInfo.hostId}`, - ); - }, - - async onContainerRestart(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} restarted on ${containerInfo.hostId}`, - ); - }, - - async onContainerUpdate(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} updated on ${containerInfo.hostId}`, - ); - }, - - async onContainerRename(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} renamed on ${containerInfo.hostId}`, - ); - }, - - async onContainerHealthStatus(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} changed status to ${containerInfo.status}`, - ); - }, - - async onHostUnreachable(host: string, err: string) { - logger.info(`Server ${host} unreachable - ${err}`); - }, - - async onHostReachableAgain(host: string) { - logger.info(`Server ${host} reachable`); - }, - - async handleContainerDie(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} died on ${containerInfo.hostId}`, - ); - }, - - async onContainerKill(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} killed on ${containerInfo.hostId}`, - ); - }, + name: "Example Plugin", + version: "1.0.0", + + async onContainerStart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} started on ${containerInfo.hostId}` + ); + }, + + async onContainerStop(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} stopped on ${containerInfo.hostId}` + ); + }, + + async onContainerExit(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} exited on ${containerInfo.hostId}` + ); + }, + + async onContainerCreate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} created on ${containerInfo.hostId}` + ); + }, + + async onContainerDestroy(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}` + ); + }, + + async onContainerPause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} pause on ${containerInfo.hostId}` + ); + }, + + async onContainerUnpause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} resumed on ${containerInfo.hostId}` + ); + }, + + async onContainerRestart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} restarted on ${containerInfo.hostId}` + ); + }, + + async onContainerUpdate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} updated on ${containerInfo.hostId}` + ); + }, + + async onContainerRename(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} renamed on ${containerInfo.hostId}` + ); + }, + + async onContainerHealthStatus(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} changed status to ${containerInfo.status}` + ); + }, + + async onHostUnreachable(host: string, err: string) { + logger.info(`Server ${host} unreachable - ${err}`); + }, + + async onHostReachableAgain(host: string) { + logger.info(`Server ${host} reachable`); + }, + + async handleContainerDie(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} died on ${containerInfo.hostId}` + ); + }, + + async onContainerKill(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} killed on ${containerInfo.hostId}` + ); + }, } satisfies Plugin; export default ExamplePlugin; diff --git a/src/plugins/telegram.plugin.ts b/src/plugins/telegram.plugin.ts index 0b83d434..5a938e71 100644 --- a/src/plugins/telegram.plugin.ts +++ b/src/plugins/telegram.plugin.ts @@ -7,29 +7,31 @@ const TELEGRAM_BOT_TOKEN = "CHANGE_ME"; // Replace with your bot token const TELEGRAM_CHAT_ID = "CHANGE_ME"; // Replace with your chat ID const TelegramNotificationPlugin: Plugin = { - name: "Telegram Notification Plugin", - async onContainerStart(containerInfo: ContainerInfo) { - const message = `Container Started: ${containerInfo.name} on ${containerInfo.hostId}`; - try { - const response = await fetch( - `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - chat_id: TELEGRAM_CHAT_ID, - text: message, - }), - }, - ); - if (!response.ok) { - logger.error(`HTTP error ${response.status}`); - } - logger.info("Telegram notification sent."); - } catch (error) { - logger.error("Failed to send Telegram notification", error as string); - } - }, + name: "Telegram Notification Plugin", + version: "1.0.0", + + async onContainerStart(containerInfo: ContainerInfo) { + const message = `Container Started: ${containerInfo.name} on ${containerInfo.hostId}`; + try { + const response = await fetch( + `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + chat_id: TELEGRAM_CHAT_ID, + text: message, + }), + } + ); + if (!response.ok) { + logger.error(`HTTP error ${response.status}`); + } + logger.info("Telegram notification sent."); + } catch (error) { + logger.error("Failed to send Telegram notification", error as string); + } + }, } satisfies Plugin; export default TelegramNotificationPlugin; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 6e2de1eb..80c07b60 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -5,581 +5,582 @@ import { backupDir } from "~/core/database/backup"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import { responseHandler } from "~/core/utils/response-handler"; import { hashApiKey } from "~/middleware/auth"; import type { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; + .get( + "", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - responses: { - "200": { - description: "Successfully retrieved configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - fetching_interval: { - type: "number", - example: 5, - }, - keep_data_for: { - type: "number", - example: 7, - }, - api_key: { - type: "string", - example: "hashed_api_key", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/plugins", - ({ set }) => { - try { - return pluginManager.getLoadedPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - responses: { - "200": { - description: "Successfully retrieved plugins", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-plugin", - }, - version: { - type: "string", - example: "1.0.0", - }, - status: { - type: "string", - example: "active", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving plugins", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting all registered plugins", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Returns current API configuration including data retention policies and security settings", + responses: { + "200": { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fetching_interval: { + type: "number", + example: 5, + }, + keep_data_for: { + type: "number", + example: 7, + }, + api_key: { + type: "string", + example: "hashed_api_key", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/plugins", + ({}) => { + try { + return pluginManager.getLoadedPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + responses: { + "200": { + description: "Successfully retrieved plugins", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-plugin", + }, + version: { + type: "string", + example: "1.0.0", + }, + status: { + type: "string", + example: "active", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving plugins", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting all registered plugins", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { fetching_interval, keep_data_for, api_key } = body; - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key), - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - responses: { - "200": { - description: "Successfully updated configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated DockStatAPI config", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error updating the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - }, - ) - .get( - "/package", - async () => { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key) + ); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", + responses: { + "200": { + description: "Successfully updated configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated DockStatAPI config", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error updating the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), + } + ) + .get( + "/package", + async () => { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json`, - ); + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json` + ); - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - responses: { - "200": { - description: "Successfully retrieved package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error while reading package.json", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .post( - "/backup", - async ({ set }) => { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return responseHandler.ok(set, backupFilename); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Backs up the internal database", - responses: { - "200": { - description: "Successfully created backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "backup_2024-03-20_12-00-00.db.bak", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error creating backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error backing up", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/backup", - async ({ set }) => { - try { - const backupFiles = readdirSync(backupDir); + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Displays package metadata including dependencies, contributors, and licensing information", + responses: { + "200": { + description: "Successfully retrieved package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0", + }, + description: { + type: "string", + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", + }, + license: { + type: "string", + example: "CC BY-NC 4.0", + }, + authorName: { + type: "string", + example: "ItsNik", + }, + authorEmail: { + type: "string", + example: "info@itsnik.de", + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik", + }, + contributors: { + type: "array", + items: { + type: "string", + }, + example: [], + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + }, + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error while reading package.json", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .post( + "/backup", + async ({ set }) => { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return responseHandler.ok(set, backupFilename); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Backs up the internal database", + responses: { + "200": { + description: "Successfully created backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "backup_2024-03-20_12-00-00.db.bak", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error creating backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error backing up", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/backup", + async ({ set }) => { + try { + const backupFiles = readdirSync(backupDir); - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Lists all available backups", - responses: { - "200": { - description: "Successfully retrieved backup list", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "string", - }, - example: [ - "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak", - ], - }, - }, - }, - }, - "400": { - description: "Error retrieving backup list", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Reading Backup directory", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Lists all available backups", + responses: { + "200": { + description: "Successfully retrieved backup list", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "string", + }, + example: [ + "backup_2024-03-20_12-00-00.db.bak", + "backup_2024-03-19_12-00-00.db.bak", + ], + }, + }, + }, + }, + "400": { + description: "Error retrieving backup list", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Reading Backup directory", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) - .get( - "/backup/download", - async ({ query, set }) => { - try { - const filename = query.filename || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; + .get( + "/backup/download", + async ({ query, set }) => { + try { + const filename = query.filename || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } - set.headers["Content-Type"] = "application/octet-stream"; - set.headers["Content-Disposition"] = - `attachment; filename="${filename}"`; - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Download a specific backup or the latest if no filename is provided", - responses: { - "200": { - description: "Successfully downloaded backup file", - content: { - "application/octet-stream": { - schema: { - type: "string", - format: "binary", - example: "Binary backup file content", - }, - }, - }, - headers: { - "Content-Disposition": { - schema: { - type: "string", - example: - 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', - }, - }, - }, - }, - "400": { - description: "Error downloading backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Backup download failed", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - filename: t.Optional(t.String()), - }), - }, - ) - .post( - "/restore", - async ({ body, set }) => { - try { - const { file } = body; + set.headers["Content-Type"] = "application/octet-stream"; + set.headers[ + "Content-Disposition" + ] = `attachment; filename="${filename}"`; + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Download a specific backup or the latest if no filename is provided", + responses: { + "200": { + description: "Successfully downloaded backup file", + content: { + "application/octet-stream": { + schema: { + type: "string", + format: "binary", + example: "Binary backup file content", + }, + }, + }, + headers: { + "Content-Disposition": { + schema: { + type: "string", + example: + 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', + }, + }, + }, + }, + "400": { + description: "Error downloading backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Backup download failed", + }, + }, + }, + }, + }, + }, + }, + }, + query: t.Object({ + filename: t.Optional(t.String()), + }), + } + ) + .post( + "/restore", + async ({ body, set }) => { + try { + const { file } = body; - set.headers["Content-Type"] = "text/html"; + set.headers["Content-Type"] = "text/html"; - if (!file) { - throw new Error("No file uploaded"); - } + if (!file) { + throw new Error("No file uploaded"); + } - if (!file.name.endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } + if (!file.name.endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); - return responseHandler.ok(set, "Database restored successfully"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - body: t.Object({ file: t.File() }), - detail: { - tags: ["Management"], - description: "Restore database from uploaded backup file", - responses: { - "200": { - description: "Successfully restored database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Database restored successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restoring database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Database restoration error", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); + return responseHandler.ok(set, "Database restored successfully"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + body: t.Object({ file: t.File() }), + detail: { + tags: ["Management"], + description: "Restore database from uploaded backup file", + responses: { + "200": { + description: "Successfully restored database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Database restored successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error restoring database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Database restoration error", + }, + }, + }, + }, + }, + }, + }, + }, + } + ); diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 51aefcd8..09df03a7 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -6,8 +6,8 @@ import split2 from "split2"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; @@ -15,122 +15,122 @@ import { responseHandler } from "~/core/utils/response-handler"; //biome-ignore lint/suspicious/noExplicitAny: const activeDockerConnections = new Set>(); const connectionStreams = new Map< - //biome-ignore lint/suspicious/noExplicitAny: - ElysiaWS, - Array<{ statsStream: Readable; splitStream: ReturnType }> + //biome-ignore lint/suspicious/noExplicitAny: + ElysiaWS, + Array<{ statsStream: Readable; splitStream: ReturnType }> >(); -export const dockerWebsocketRoutes = new Elysia({ prefix: "/docker" }).ws( - "/stats", - { - async open(ws) { - activeDockerConnections.add(ws); - connectionStreams.set(ws, []); +export const dockerWebsocketRoutes = new Elysia({ prefix: "/ws" }).ws( + "/docker", + { + async open(ws) { + activeDockerConnections.add(ws); + connectionStreams.set(ws, []); - ws.send(JSON.stringify({ message: "Connection established" })); - logger.info(`New Docker WebSocket established (${ws.id})`); + ws.send(JSON.stringify({ message: "Connection established" })); + logger.info(`New Docker WebSocket established (${ws.id})`); - try { - const hosts = dbFunctions.getDockerHosts(); - logger.debug(`Retrieved ${hosts.length} docker host(s)`); + try { + const hosts = dbFunctions.getDockerHosts(); + logger.debug(`Retrieved ${hosts.length} docker host(s)`); - for (const host of hosts) { - if (ws.readyState !== 1) { - break; - } + for (const host of hosts) { + if (ws.readyState !== 1) { + break; + } - const docker = getDockerClient(host); - await docker.ping(); - const containers = await docker.listContainers({ all: true }); - logger.debug( - `Found ${containers.length} containers on ${host.name} (id: ${host.id})`, - ); + const docker = getDockerClient(host); + await docker.ping(); + const containers = await docker.listContainers({ all: true }); + logger.debug( + `Found ${containers.length} containers on ${host.name} (id: ${host.id})` + ); - for (const containerInfo of containers) { - if (ws.readyState !== 1) { - break; - } + for (const containerInfo of containers) { + if (ws.readyState !== 1) { + break; + } - const container = docker.getContainer(containerInfo.Id); - const statsStream = (await container.stats({ - stream: true, - })) as Readable; - const splitStream = split2(); + const container = docker.getContainer(containerInfo.Id); + const statsStream = (await container.stats({ + stream: true, + })) as Readable; + const splitStream = split2(); - connectionStreams.get(ws)?.push({ statsStream, splitStream }); + connectionStreams.get(ws)?.push({ statsStream, splitStream }); - statsStream - .on("close", () => splitStream.destroy()) - .pipe(splitStream) - .on("data", (line: string) => { - if (ws.readyState !== 1 || !line) { - return; - } - try { - const stats = JSON.parse(line); - ws.send( - JSON.stringify({ - id: containerInfo.Id, - hostId: host.id, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats) || 0, - memoryUsage: calculateMemoryUsage(stats) || 0, - }), - ); - } catch (error) { - logger.error(`Parse error: ${error}`); - } - }) - .on("error", (error: Error) => { - logger.error(`Stream error: ${error}`); - statsStream.destroy(); - ws.send( - JSON.stringify({ - hostId: host.name, - containerId: containerInfo.Id, - error: `Stats stream error: ${error}`, - }), - ); - }); - } - } - } catch (error) { - logger.error(`Connection error: ${error}`); - ws.send( - JSON.stringify( - responseHandler.error( - { headers: {} }, - error as string, - "Docker connection failed", - 500, - ), - ), - ); - } - }, + statsStream + .on("close", () => splitStream.destroy()) + .pipe(splitStream) + .on("data", (line: string) => { + if (ws.readyState !== 1 || !line) { + return; + } + try { + const stats = JSON.parse(line); + ws.send( + JSON.stringify({ + id: containerInfo.Id, + hostId: host.id, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats) || 0, + memoryUsage: calculateMemoryUsage(stats) || 0, + }) + ); + } catch (error) { + logger.error(`Parse error: ${error}`); + } + }) + .on("error", (error: Error) => { + logger.error(`Stream error: ${error}`); + statsStream.destroy(); + ws.send( + JSON.stringify({ + hostId: host.name, + containerId: containerInfo.Id, + error: `Stats stream error: ${error}`, + }) + ); + }); + } + } + } catch (error) { + logger.error(`Connection error: ${error}`); + ws.send( + JSON.stringify( + responseHandler.error( + { headers: {} }, + error as string, + "Docker connection failed", + 500 + ) + ) + ); + } + }, - message(ws, message) { - if (message === "pong") ws.pong(); - }, + message(ws, message) { + if (message === "pong") ws.pong(); + }, - close(ws) { - logger.info(`Closing connection ${ws.id}`); - activeDockerConnections.delete(ws); + close(ws) { + logger.info(`Closing connection ${ws.id}`); + activeDockerConnections.delete(ws); - const streams = connectionStreams.get(ws) || []; - for (const { statsStream, splitStream } of streams) { - try { - statsStream.unpipe(splitStream); - statsStream.destroy(); - splitStream.destroy(); - } catch (error) { - logger.error(`Cleanup error: ${error}`); - } - } - connectionStreams.delete(ws); - }, - }, + const streams = connectionStreams.get(ws) || []; + for (const { statsStream, splitStream } of streams) { + try { + statsStream.unpipe(splitStream); + statsStream.destroy(); + splitStream.destroy(); + } catch (error) { + logger.error(`Cleanup error: ${error}`); + } + } + connectionStreams.delete(ws); + }, + } ); diff --git a/src/routes/live-logs.ts b/src/routes/live-logs.ts index cf6e4b5b..5c451fcf 100644 --- a/src/routes/live-logs.ts +++ b/src/routes/live-logs.ts @@ -8,31 +8,31 @@ import type { log_message } from "~/typings/database"; //biome-ignore lint/suspicious/noExplicitAny: const activeConnections = new Set>(); -export const liveLogs = new Elysia({ prefix: "/logs" }).ws("/ws", { - open(ws) { - activeConnections.add(ws); - ws.send({ - message: "Connection established", - level: "info", - timestamp: new Date().toISOString(), - file: "live-logs.ts", - line: 14, - }); - logger.info(`New Logs WebSocket established (${ws.id})`); - }, - close(ws) { - logger.info(`Logs WebSocket closed (${ws.id})`); - activeConnections.delete(ws); - }, +export const liveLogs = new Elysia({ prefix: "/ws" }).ws("/logs", { + open(ws) { + activeConnections.add(ws); + ws.send({ + message: "Connection established", + level: "info", + timestamp: new Date().toISOString(), + file: "live-logs.ts", + line: 14, + }); + logger.info(`New Logs WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Logs WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, }); export function logToClients(data: log_message) { - for (const ws of activeConnections) { - try { - ws.send(JSON.stringify(data)); - } catch (error) { - activeConnections.delete(ws); - logger.error("Failed to send to WebSocket:", error); - } - } + for (const ws of activeConnections) { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + } } diff --git a/src/routes/live-stacks.ts b/src/routes/live-stacks.ts index 77aa2857..a841678e 100644 --- a/src/routes/live-stacks.ts +++ b/src/routes/live-stacks.ts @@ -6,25 +6,25 @@ import type { stackSocketMessage } from "~/typings/websocket"; //biome-ignore lint/suspicious/noExplicitAny: Any = Connections const activeConnections = new Set>(); -export const liveStacks = new Elysia().ws("/stacks", { - open(ws) { - activeConnections.add(ws); - ws.send({ message: "Connection established" }); - logger.info(`New Stacks WebSocket established (${ws.id})`); - }, - close(ws) { - logger.info(`Stacks WebSocket closed (${ws.id})`); - activeConnections.delete(ws); - }, +export const liveStacks = new Elysia({ prefix: "/ws" }).ws("/stacks", { + open(ws) { + activeConnections.add(ws); + ws.send({ message: "Connection established" }); + logger.info(`New Stacks WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Stacks WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, }); export function postToClient(data: stackSocketMessage) { - for (const ws of activeConnections) { - try { - ws.send(JSON.stringify(data)); - } catch (error) { - activeConnections.delete(ws); - logger.error("Failed to send to WebSocket:", error); - } - } + for (const ws of activeConnections) { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + } } diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index 3ac2b86d..b504e4d8 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -1,598 +1,598 @@ import { Elysia, t } from "elysia"; import { dbFunctions } from "~/core/database"; import { - deployStack, - getAllStacksStatus, - getStackStatus, - pullStackImages, - removeStack, - restartStack, - startStack, - stopStack, + deployStack, + getAllStacksStatus, + getStackStatus, + pullStackImages, + removeStack, + restartStack, + startStack, + stopStack, } from "~/core/stacks/controller"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; import type { stacks_config } from "~/typings/database"; export const stackRoutes = new Elysia({ prefix: "/stacks" }) - .post( - "/deploy", - async ({ set, body }) => { - try { - await deployStack(body as stacks_config); - logger.info(`Deployed Stack (${body.name})`); - return responseHandler.ok( - set, - `Stack ${body.name} deployed successfully`, - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + .post( + "/deploy", + async ({ set, body }) => { + try { + await deployStack(body as stacks_config); + logger.info(`Deployed Stack (${body.name})`); + return responseHandler.ok( + set, + `Stack ${body.name} deployed successfully` + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error( - set, - errorMsg, - "Error deploying stack, please check the server logs for more information", - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Deploys a new Docker stack using a provided compose specification, allowing custom configurations and image updates", - responses: { - "200": { - description: "Successfully deployed stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack example-stack deployed successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error deploying stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error deploying stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - name: t.String(), - version: t.Number(), - custom: t.Boolean(), - source: t.String(), - compose_spec: t.Any(), - }), - }, - ) - .post( - "/start", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack ID needed"); - } - await startStack(body.stackId); - logger.info(`Started Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} started successfully`, - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + return responseHandler.error( + set, + errorMsg, + "Error deploying stack, please check the server logs for more information" + ); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Deploys a new Docker stack using a provided compose specification, allowing custom configurations and image updates", + responses: { + "200": { + description: "Successfully deployed stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack example-stack deployed successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error deploying stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error deploying stack", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + name: t.String(), + version: t.Number(), + custom: t.Boolean(), + source: t.String(), + compose_spec: t.Any(), + }), + } + ) + .post( + "/start", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack ID needed"); + } + await startStack(body.stackId); + logger.info(`Started Stack (${body.stackId})`); + return responseHandler.ok( + set, + `Stack ${body.stackId} started successfully` + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error(set, errorMsg, "Error starting stack"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Initiates a Docker stack, starting all associated containers", - responses: { - "200": { - description: "Successfully started stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 started successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error starting stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error starting stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - }, - ) - .post( - "/stop", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await stopStack(body.stackId); - logger.info(`Stopped Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} stopped successfully`, - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + return responseHandler.error(set, errorMsg, "Error starting stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Initiates a Docker stack, starting all associated containers", + responses: { + "200": { + description: "Successfully started stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 started successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error starting stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error starting stack", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + stackId: t.Number(), + }), + } + ) + .post( + "/stop", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack needed"); + } + await stopStack(body.stackId); + logger.info(`Stopped Stack (${body.stackId})`); + return responseHandler.ok( + set, + `Stack ${body.stackId} stopped successfully` + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error(set, errorMsg, "Error stopping stack"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Halts a running Docker stack and its containers while preserving configurations", - responses: { - "200": { - description: "Successfully stopped stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 stopped successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error stopping stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error stopping stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - }, - ) - .post( - "/restart", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await restartStack(body.stackId); - logger.info(`Restarted Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} restarted successfully`, - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + return responseHandler.error(set, errorMsg, "Error stopping stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Halts a running Docker stack and its containers while preserving configurations", + responses: { + "200": { + description: "Successfully stopped stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 stopped successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error stopping stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error stopping stack", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + stackId: t.Number(), + }), + } + ) + .post( + "/restart", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack needed"); + } + await restartStack(body.stackId); + logger.info(`Restarted Stack (${body.stackId})`); + return responseHandler.ok( + set, + `Stack ${body.stackId} restarted successfully` + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error(set, errorMsg, "Error restarting stack"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Performs full stack restart - stops and restarts all stack components in sequence", - responses: { - "200": { - description: "Successfully restarted stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 restarted successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restarting stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error restarting stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - }, - ) - .post( - "/pull-images", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await pullStackImages(body.stackId); - logger.info(`Pulled Stack images (${body.stackId})`); - return responseHandler.ok( - set, - `Images for stack ${body.stackId} pulled successfully`, - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + return responseHandler.error(set, errorMsg, "Error restarting stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Performs full stack restart - stops and restarts all stack components in sequence", + responses: { + "200": { + description: "Successfully restarted stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 restarted successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error restarting stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error restarting stack", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + stackId: t.Number(), + }), + } + ) + .post( + "/pull-images", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack needed"); + } + await pullStackImages(body.stackId); + logger.info(`Pulled Stack images (${body.stackId})`); + return responseHandler.ok( + set, + `Images for stack ${body.stackId} pulled successfully` + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error(set, errorMsg, "Error pulling images"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Updates container images for a stack using Docker's pull mechanism (requires stack ID)", - responses: { - "200": { - description: "Successfully pulled images", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Images for stack 1 pulled successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error pulling images", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error pulling images", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - }, - ) - .get( - "/status", - async ({ set, query }) => { - try { - // biome-ignore lint/suspicious/noExplicitAny: - let status: Record; - let res = {}; + return responseHandler.error(set, errorMsg, "Error pulling images"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Updates container images for a stack using Docker's pull mechanism (requires stack ID)", + responses: { + "200": { + description: "Successfully pulled images", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Images for stack 1 pulled successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error pulling images", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error pulling images", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + stackId: t.Number(), + }), + } + ) + .get( + "/status", + async ({ set, query }) => { + try { + // biome-ignore lint/suspicious/noExplicitAny: + let status: Record; + let res = {}; - logger.debug("Entering stack status handler"); - logger.debug(`Request body: ${JSON.stringify(query)}`); + logger.debug("Entering stack status handler"); + logger.debug(`Request body: ${JSON.stringify(query)}`); - if (query.stackId) { - logger.debug(`Fetching status for stackId=${query.stackId}`); - status = await getStackStatus(query.stackId); - logger.debug( - `Retrieved status for stackId=${query.stackId}: ${JSON.stringify(status)}`, - ); + if (query.stackId !== 0) { + logger.debug(`Fetching status for stackId=${query.stackId}`); + status = await getStackStatus(query.stackId); + logger.debug( + `Retrieved status for stackId=${query.stackId}: ${JSON.stringify( + status + )}` + ); - res = responseHandler.ok( - set, - `Stack ${query.stackId} status retrieved successfully`, - ); - logger.info("Fetched Stack status"); - } else { - logger.debug("Fetching status for all stacks"); - status = await getAllStacksStatus(); - logger.debug( - `Retrieved status for all stacks: ${JSON.stringify(status)}`, - ); + res = responseHandler.ok( + set, + `Stack ${query.stackId} status retrieved successfully` + ); + logger.info("Fetched Stack status"); + } else { + logger.debug("Fetching status for all stacks"); + status = await getAllStacksStatus(); + logger.debug( + `Retrieved status for all stacks: ${JSON.stringify(status)}` + ); - res = responseHandler.ok(set, "Fetched all Stack's status"); - logger.info("Fetched all Stack status"); - } + res = responseHandler.ok(set, "Fetched all Stack's status"); + logger.info("Fetched all Stack status"); + } - logger.debug("Returning response with status data"); - return { ...res, status: status }; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.debug(`Error occurred while fetching stack status: ${errorMsg}`); + logger.debug("Returning response with status data"); + return { ...res, status: status }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.debug(`Error occurred while fetching stack status: ${errorMsg}`); - return responseHandler.error( - set, - errorMsg, - "Error getting stack status", - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Retrieves operational status for either a specific stack (by ID) or all managed stacks", - responses: { - "200": { - description: "Successfully retrieved stack status", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 status retrieved successfully", - }, - status: { - type: "object", - properties: { - name: { - type: "string", - example: "example-stack", - }, - status: { - type: "string", - example: "running", - }, - containers: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-stack_web_1", - }, - status: { - type: "string", - example: "running", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error getting stack status", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting stack status", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Optional( - t.Object({ - stackId: t.Number(), - }), - ), - }, - ) - .get( - "/", - async ({ set }) => { - try { - const stacks = dbFunctions.getStacks(); - logger.info("Fetched Stacks"); - return stacks; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + return responseHandler.error( + set, + errorMsg, + "Error getting stack status" + ); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Retrieves operational status for either a specific stack (by ID) or all managed stacks (ID: 0)", + responses: { + "200": { + description: "Successfully retrieved stack status", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 status retrieved successfully", + }, + status: { + type: "object", + properties: { + name: { + type: "string", + example: "example-stack", + }, + status: { + type: "string", + example: "running", + }, + containers: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-stack_web_1", + }, + status: { + type: "string", + example: "running", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error getting stack status", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting stack status", + }, + }, + }, + }, + }, + }, + }, + }, + query: t.Object({ + stackId: t.Number(), + }), + } + ) + .get( + "/", + async ({ set }) => { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks"); + return stacks; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error(set, errorMsg, "Error getting stacks"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Lists all registered stacks with their complete configuration details", - responses: { - "200": { - description: "Successfully retrieved stacks", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "number", - example: 1, - }, - name: { - type: "string", - example: "example-stack", - }, - version: { - type: "number", - example: 1, - }, - source: { - type: "string", - example: "github.com/example/repo", - }, - automatic_reboot_on_error: { - type: "boolean", - example: true, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error getting stacks", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting stacks", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .delete( - "/", - async ({ set, body }) => { - try { - const { stackId } = body; - await removeStack(stackId); - logger.info(`Deleted Stack ${stackId}`); - return responseHandler.ok(set, `Stack ${stackId} deleted successfully`); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + return responseHandler.error(set, errorMsg, "Error getting stacks"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Lists all registered stacks with their complete configuration details", + responses: { + "200": { + description: "Successfully retrieved stacks", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "number", + example: 1, + }, + name: { + type: "string", + example: "example-stack", + }, + version: { + type: "number", + example: 1, + }, + source: { + type: "string", + example: "github.com/example/repo", + }, + automatic_reboot_on_error: { + type: "boolean", + example: true, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error getting stacks", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting stacks", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .delete( + "/", + async ({ set, body }) => { + try { + const { stackId } = body; + await removeStack(stackId); + logger.info(`Deleted Stack ${stackId}`); + return responseHandler.ok(set, `Stack ${stackId} deleted successfully`); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error(set, errorMsg, "Error deleting stack"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Permanently removes a stack configuration and cleans up associated resources", - responses: { - "200": { - description: "Successfully deleted stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 deleted successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error deleting stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error deleting stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - }, - ); + return responseHandler.error(set, errorMsg, "Error deleting stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Permanently removes a stack configuration and cleans up associated resources", + responses: { + "200": { + description: "Successfully deleted stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 deleted successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error deleting stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error deleting stack", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + stackId: t.Number(), + }), + } + ); diff --git a/src/typings b/src/typings index 9cae829b..d0d22fa6 160000 --- a/src/typings +++ b/src/typings @@ -1 +1 @@ -Subproject commit 9cae829bead60cd13351b757340f3225649cb11d +Subproject commit d0d22fa622c5dd9d298d358d4215c8b54cb5f4f3 From ec0c79abe3f2f55fe80b220305eb96f19f03c173 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Mon, 2 Jun 2025 08:29:32 +0200 Subject: [PATCH 326/369] Fix: Linter fix --- src/core/database/stacks.ts | 66 +- src/core/plugins/plugin-manager.ts | 274 +++---- src/core/stacks/controller.ts | 700 ++++++++--------- src/core/utils/helpers.ts | 16 +- src/plugins/example.plugin.ts | 176 ++--- src/plugins/telegram.plugin.ts | 48 +- src/routes/api-config.ts | 1113 ++++++++++++++------------- src/routes/docker-websocket.ts | 212 ++--- src/routes/live-logs.ts | 46 +- src/routes/live-stacks.ts | 34 +- src/routes/stacks.ts | 1152 ++++++++++++++-------------- 11 files changed, 1918 insertions(+), 1919 deletions(-) diff --git a/src/core/database/stacks.ts b/src/core/database/stacks.ts index a442482b..e872bbcf 100644 --- a/src/core/database/stacks.ts +++ b/src/core/database/stacks.ts @@ -5,62 +5,62 @@ import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare(` + insert: db.prepare(` INSERT INTO stacks_config ( name, version, custom, source, compose_spec ) VALUES (?, ?, ?, ?, ?) `), - selectAll: db.prepare(` + selectAll: db.prepare(` SELECT id, name, version, custom, source, compose_spec FROM stacks_config ORDER BY name DESC `), - update: db.prepare(` + update: db.prepare(` UPDATE stacks_config SET name = ?, custom = ?, source = ?, compose_spec = ? WHERE name = ? `), - delete: db.prepare("DELETE FROM stacks_config WHERE id = ?"), + delete: db.prepare("DELETE FROM stacks_config WHERE id = ?"), }; export function addStack(stack: stacks_config) { - executeDbOperation("Add Stack", () => - stmt.insert.run( - stack.name, - stack.version, - stack.custom, - stack.source, - stack.compose_spec - ) - ); + executeDbOperation("Add Stack", () => + stmt.insert.run( + stack.name, + stack.version, + stack.custom, + stack.source, + stack.compose_spec, + ), + ); - return findObjectByKey(getStacks(), "name", stack.name)?.id; + return findObjectByKey(getStacks(), "name", stack.name)?.id; } export function getStacks() { - return executeDbOperation("Get Stacks", () => - stmt.selectAll.all() - ) as Stack[]; + return executeDbOperation("Get Stacks", () => + stmt.selectAll.all(), + ) as Stack[]; } export function deleteStack(id: number) { - return executeDbOperation( - "Delete Stack", - () => stmt.delete.run(id), - () => { - if (typeof id !== "number") throw new TypeError("Invalid stack ID"); - } - ); + return executeDbOperation( + "Delete Stack", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid stack ID"); + }, + ); } export function updateStack(stack: stacks_config) { - return executeDbOperation("Update Stack", () => - stmt.update.run( - stack.version, - stack.custom, - stack.source, - stack.name, - stack.compose_spec - ) - ); + return executeDbOperation("Update Stack", () => + stmt.update.run( + stack.version, + stack.custom, + stack.source, + stack.name, + stack.compose_spec, + ), + ); } diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index ad9bfeee..2650e8c7 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -1,145 +1,145 @@ import { EventEmitter } from "node:events"; import type { ContainerInfo } from "~/typings/docker"; -import type { Plugin, Hooks, PluginInfo } from "~/typings/plugin"; +import type { Hooks, Plugin, PluginInfo } from "~/typings/plugin"; import { logger } from "../utils/logger"; class PluginManager extends EventEmitter { - private plugins: Map = new Map(); - - register(plugin: Plugin) { - try { - this.plugins.set(plugin.name, plugin); - logger.debug(`Registered plugin: ${plugin.name}`); - } catch (error) { - logger.error( - `Registering plugin ${plugin.name} failed: ${error as string}` - ); - } - } - - unregister(name: string) { - this.plugins.delete(name); - } - - getLoadedPlugins(): PluginInfo[] { - return Array.from(this.plugins.values()).map((plugin) => { - const hooks: Hooks = { - onContainerStart: !!plugin.onContainerStart, - onContainerStop: !!plugin.onContainerStop, - onContainerExit: !!plugin.onContainerExit, - onContainerCreate: !!plugin.onContainerCreate, - onContainerKill: !!plugin.onContainerKill, - handleContainerDie: !!plugin.handleContainerDie, - onContainerDestroy: !!plugin.onContainerDestroy, - onContainerPause: !!plugin.onContainerPause, - onContainerUnpause: !!plugin.onContainerUnpause, - onContainerRestart: !!plugin.onContainerRestart, - onContainerUpdate: !!plugin.onContainerUpdate, - onContainerRename: !!plugin.onContainerRename, - onContainerHealthStatus: !!plugin.onContainerHealthStatus, - onHostUnreachable: !!plugin.onHostUnreachable, - onHostReachableAgain: !!plugin.onHostReachableAgain, - }; - - return { - name: plugin.name, - version: plugin.version, - status: "active", - usedHooks: hooks, - }; - }); - } - - // Trigger plugin flows: - handleContainerStop(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerStop?.(containerInfo); - } - } - - handleContainerStart(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerStart?.(containerInfo); - } - } - - handleContainerExit(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerExit?.(containerInfo); - } - } - - handleContainerCreate(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerCreate?.(containerInfo); - } - } - - handleContainerDestroy(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerDestroy?.(containerInfo); - } - } - - handleContainerPause(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerPause?.(containerInfo); - } - } - - handleContainerUnpause(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerUnpause?.(containerInfo); - } - } - - handleContainerRestart(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerRestart?.(containerInfo); - } - } - - handleContainerUpdate(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerUpdate?.(containerInfo); - } - } - - handleContainerRename(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerRename?.(containerInfo); - } - } - - handleContainerHealthStatus(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerHealthStatus?.(containerInfo); - } - } - - handleHostUnreachable(host: string, err: string) { - for (const [, plugin] of this.plugins) { - plugin.onHostUnreachable?.(host, err); - } - } - - handleHostReachableAgain(host: string) { - for (const [, plugin] of this.plugins) { - plugin.onHostReachableAgain?.(host); - } - } - - handleContainerKill(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerKill?.(containerInfo); - } - } - - handleContainerDie(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.handleContainerDie?.(containerInfo); - } - } + private plugins: Map = new Map(); + + register(plugin: Plugin) { + try { + this.plugins.set(plugin.name, plugin); + logger.debug(`Registered plugin: ${plugin.name}`); + } catch (error) { + logger.error( + `Registering plugin ${plugin.name} failed: ${error as string}`, + ); + } + } + + unregister(name: string) { + this.plugins.delete(name); + } + + getLoadedPlugins(): PluginInfo[] { + return Array.from(this.plugins.values()).map((plugin) => { + const hooks: Hooks = { + onContainerStart: !!plugin.onContainerStart, + onContainerStop: !!plugin.onContainerStop, + onContainerExit: !!plugin.onContainerExit, + onContainerCreate: !!plugin.onContainerCreate, + onContainerKill: !!plugin.onContainerKill, + handleContainerDie: !!plugin.handleContainerDie, + onContainerDestroy: !!plugin.onContainerDestroy, + onContainerPause: !!plugin.onContainerPause, + onContainerUnpause: !!plugin.onContainerUnpause, + onContainerRestart: !!plugin.onContainerRestart, + onContainerUpdate: !!plugin.onContainerUpdate, + onContainerRename: !!plugin.onContainerRename, + onContainerHealthStatus: !!plugin.onContainerHealthStatus, + onHostUnreachable: !!plugin.onHostUnreachable, + onHostReachableAgain: !!plugin.onHostReachableAgain, + }; + + return { + name: plugin.name, + version: plugin.version, + status: "active", + usedHooks: hooks, + }; + }); + } + + // Trigger plugin flows: + handleContainerStop(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStop?.(containerInfo); + } + } + + handleContainerStart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStart?.(containerInfo); + } + } + + handleContainerExit(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerExit?.(containerInfo); + } + } + + handleContainerCreate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerCreate?.(containerInfo); + } + } + + handleContainerDestroy(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerDestroy?.(containerInfo); + } + } + + handleContainerPause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerPause?.(containerInfo); + } + } + + handleContainerUnpause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUnpause?.(containerInfo); + } + } + + handleContainerRestart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRestart?.(containerInfo); + } + } + + handleContainerUpdate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUpdate?.(containerInfo); + } + } + + handleContainerRename(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRename?.(containerInfo); + } + } + + handleContainerHealthStatus(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerHealthStatus?.(containerInfo); + } + } + + handleHostUnreachable(host: string, err: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostUnreachable?.(host, err); + } + } + + handleHostReachableAgain(host: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostReachableAgain?.(host); + } + } + + handleContainerKill(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerKill?.(containerInfo); + } + } + + handleContainerDie(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.handleContainerDie?.(containerInfo); + } + } } export const pluginManager = new PluginManager(); diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 60047f8a..637a7e5d 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -9,409 +9,409 @@ import type { ComposeSpec, Stack } from "~/typings/docker-compose"; import { findObjectByKey } from "../utils/helpers"; const wrapProgressCallback = (progressCallback?: (log: string) => void) => { - return progressCallback - ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { - const log = chunk.toString(); - progressCallback(log); - } - : undefined; + return progressCallback + ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; }; async function getStackName(stack_id: number): Promise { - logger.debug(`Fetching stack name for id ${stack_id}`); - const stacks = dbFunctions.getStacks(); - const stack = findObjectByKey(stacks, "id", stack_id); - if (!stack) { - throw new Error(`Stack with id ${stack_id} not found`); - } - return stack.name; + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = findObjectByKey(stacks, "id", stack_id); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; } async function runStackCommand( - stack_id: number, - command: ( - cwd: string, - progressCallback?: (log: string) => void - ) => Promise, - action: string + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void, + ) => Promise, + action: string, ): Promise { - try { - logger.debug( - `Starting runStackCommand for stack_id=${stack_id}, action="${action}"` - ); - - const stackName = await getStackName(stack_id); - logger.debug( - `Retrieved stack name "${stackName}" for stack_id=${stack_id}` - ); - - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); - - const progressCallback = (log: string) => { - const message = log.trim(); - logger.debug( - `Progress for stack_id=${stack_id}, action="${action}": ${message}` - ); - - // ERROR HANDLING FOR COMPOSE ACTIONS - if (message.includes("Error response from daemon")) { - logger.error( - `Error response from daemon: ${ - message.split("Error response from daemon:")[1] - }` - ); - } - - postToClient({ - type: "stack-progress", - data: { - stack_id, - action, - message, - timestamp: new Date().toISOString(), - }, - }); - }; - - logger.debug( - `Executing command for stack_id=${stack_id}, action="${action}"` - ); - const result = await command(stackPath, progressCallback); - logger.debug( - `Successfully completed command for stack_id=${stack_id}, action="${action}"` - ); - - return result; - } catch (error) { - logger.debug( - `Error occurred for stack_id=${stack_id}, action="${action}": ${String( - error - )}` - ); - postToClient({ - type: "stack-error", - data: { - stack_id, - action, - message: String(error), - timestamp: new Date().toISOString(), - }, - }); - throw new Error( - `Error while ${action} stack "${stack_id}": ${String(error)}` - ); - } + try { + logger.debug( + `Starting runStackCommand for stack_id=${stack_id}, action="${action}"`, + ); + + const stackName = await getStackName(stack_id); + logger.debug( + `Retrieved stack name "${stackName}" for stack_id=${stack_id}`, + ); + + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); + + const progressCallback = (log: string) => { + const message = log.trim(); + logger.debug( + `Progress for stack_id=${stack_id}, action="${action}": ${message}`, + ); + + // ERROR HANDLING FOR COMPOSE ACTIONS + if (message.includes("Error response from daemon")) { + logger.error( + `Error response from daemon: ${ + message.split("Error response from daemon:")[1] + }`, + ); + } + + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message, + timestamp: new Date().toISOString(), + }, + }); + }; + + logger.debug( + `Executing command for stack_id=${stack_id}, action="${action}"`, + ); + const result = await command(stackPath, progressCallback); + logger.debug( + `Successfully completed command for stack_id=${stack_id}, action="${action}"`, + ); + + return result; + } catch (error) { + logger.debug( + `Error occurred for stack_id=${stack_id}, action="${action}": ${String( + error, + )}`, + ); + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: String(error), + timestamp: new Date().toISOString(), + }, + }); + throw new Error( + `Error while ${action} stack "${stack_id}": ${String(error)}`, + ); + } } async function getStackPath(stack: Stack): Promise { - const stackName = stack.name.trim().replace(/\s+/g, "_"); - const stackId = stack.id; + const stackName = stack.name.trim().replace(/\s+/g, "_"); + const stackId = stack.id; - if (!stackId) { - logger.error("Stack could not be parsed"); - throw new Error("Stack could not be parsed"); - } + if (!stackId) { + logger.error("Stack could not be parsed"); + throw new Error("Stack could not be parsed"); + } - return `stacks/${stackId}-${stackName}`; + return `stacks/${stackId}-${stackName}`; } async function createStackYAML(compose_spec: Stack): Promise { - const yaml = YAML.stringify(compose_spec.compose_spec); - const stackPath = await getStackPath(compose_spec); - await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { - createPath: true, - }); + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); } export async function deployStack(stack_config: stacks_config): Promise { - try { - logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); - - if (!stack_config.name) { - throw new Error("Stack name needed"); - } - - const jsonStringStack = { - ...stack_config, - compose_spec: JSON.stringify(stack_config.compose_spec), - }; - - const stackId = dbFunctions.addStack(jsonStringStack); - - if (!stackId) { - throw new Error("Failed to add stack to database"); - } - - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "pending", - message: "Creating stack configuration", - }, - }); - - const stackYaml: Stack = { - id: stackId, - name: stack_config.name, - source: stack_config.source, - version: stack_config.version, - compose_spec: stack_config.compose_spec as unknown as ComposeSpec, // Weird stuff i am doing here... smh - }; - - await createStackYAML(stackYaml); - - await runStackCommand( - stackId, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "deploying" - ); - - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "deployed", - message: "Stack deployed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id: 0, - action: "deploying", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); + + if (!stack_config.name) { + throw new Error("Stack name needed"); + } + + const jsonStringStack = { + ...stack_config, + compose_spec: JSON.stringify(stack_config.compose_spec), + }; + + const stackId = dbFunctions.addStack(jsonStringStack); + + if (!stackId) { + throw new Error("Failed to add stack to database"); + } + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }); + + const stackYaml: Stack = { + id: stackId, + name: stack_config.name, + source: stack_config.source, + version: stack_config.version, + compose_spec: stack_config.compose_spec as unknown as ComposeSpec, // Weird stuff i am doing here... smh + }; + + await createStackYAML(stackYaml); + + await runStackCommand( + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying", + ); + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id: 0, + action: "deploying", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } export async function stopStack(stack_id: number): Promise { - // Note the await to discard the result (convert to void) - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.downAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "stopping" - ); + // Note the await to discard the result (convert to void) + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.downAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping", + ); } export async function startStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "starting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting", + ); } export async function pullStackImages(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.pullAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "pulling-images" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images", + ); } export async function restartStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.restartAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "restarting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting", + ); } export async function getStackStatus( - stack_id: number - //biome-ignore lint/suspicious/noExplicitAny: + stack_id: number, + //biome-ignore lint/suspicious/noExplicitAny: ): Promise> { - const status = await runStackCommand( - stack_id, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - //biome-ignore lint/suspicious/noExplicitAny: - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check" - ); - return status; + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check", + ); + return status; } export async function removeStack(stack_id: number): Promise { - try { - const _ = dbFunctions.deleteStack(stack_id); - - await runStackCommand( - stack_id, - async (cwd, progressCallback) => { - await DockerCompose.down({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }); - }, - "removing" - ); - - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - - try { - await rm(stackPath, { recursive: true }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } - - postToClient({ - type: "stack-removed", - data: { - stack_id, - message: "Stack removed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + try { + const _ = dbFunctions.deleteStack(stack_id); + + await runStackCommand( + stack_id, + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }); + }, + "removing", + ); + + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + + try { + await rm(stackPath, { recursive: true }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } + + postToClient({ + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } interface DockerServiceStatus { - status: string; - ports: string[]; + status: string; + ports: string[]; } interface StackStatus { - services: Record; - healthy: number; - unhealthy: number; - total: number; + services: Record; + healthy: number; + unhealthy: number; + total: number; } type StacksStatus = Record; export async function getAllStacksStatus(): Promise { - try { - const stacks = dbFunctions.getStacks(); - - const statusResults = await Promise.all( - stacks.map(async (stack) => { - const status = await runStackCommand( - stack.id as number, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - const services = rawStatus.data.services.reduce( - (acc: Record, service) => { - acc[service.name] = { - status: service.state, - ports: service.ports.map( - (port) => `${port.mapped?.address}:${port.mapped?.port}` - ), - }; - return acc; - }, - {} - ); - - const statusValues = Object.values(services); - return { - services, - healthy: statusValues.filter( - (s) => s.status === "running" || s.status.includes("Up") - ).length, - unhealthy: statusValues.filter( - (s) => s.status !== "running" && !s.status.includes("Up") - ).length, - total: statusValues.length, - }; - }, - "status-check" - ); - return { stackId: stack.id, status }; - }) - ); - - return statusResults.reduce((acc, { stackId, status }) => { - acc[String(stackId)] = status; - return acc; - }, {} as StacksStatus); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + try { + const stacks = dbFunctions.getStacks(); + + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.id as number, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + const services = rawStatus.data.services.reduce( + (acc: Record, service) => { + acc[service.name] = { + status: service.state, + ports: service.ports.map( + (port) => `${port.mapped?.address}:${port.mapped?.port}`, + ), + }; + return acc; + }, + {}, + ); + + const statusValues = Object.values(services); + return { + services, + healthy: statusValues.filter( + (s) => s.status === "running" || s.status.includes("Up"), + ).length, + unhealthy: statusValues.filter( + (s) => s.status !== "running" && !s.status.includes("Up"), + ).length, + total: statusValues.length, + }; + }, + "status-check", + ); + return { stackId: stack.id, status }; + }), + ); + + return statusResults.reduce((acc, { stackId, status }) => { + acc[String(stackId)] = status; + return acc; + }, {} as StacksStatus); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } async function backupStack(stackId: number) { - if (!stackId) { - throw new Error("No Stack ID provided"); - } + if (!stackId) { + throw new Error("No Stack ID provided"); + } - const stacks = dbFunctions.getStacks(); + const stacks = dbFunctions.getStacks(); - const stack = findObjectByKey(stacks, "id", stackId); + const stack = findObjectByKey(stacks, "id", stackId); - if (!stack) { - throw new Error(`No Stack with Id: ${stackId} found`); - } + if (!stack) { + throw new Error(`No Stack with Id: ${stackId} found`); + } - const stack_path = `${stack.id}-${stack.name}`; + const stack_path = `${stack.id}-${stack.name}`; } diff --git a/src/core/utils/helpers.ts b/src/core/utils/helpers.ts index 232f0244..989fb993 100644 --- a/src/core/utils/helpers.ts +++ b/src/core/utils/helpers.ts @@ -10,13 +10,13 @@ import { logger } from "./logger"; * @returns {T | undefined} The first matching object, or undefined if no match is found. */ export function findObjectByKey( - array: T[], - key: keyof T, - value: T[keyof T] + array: T[], + key: keyof T, + value: T[keyof T], ): T | undefined { - logger.debug( - `Searching for key: ${String(key)} with value: ${String(value)}` - ); - const data = array.find((item) => item[key] === value); - return data; + logger.debug( + `Searching for key: ${String(key)} with value: ${String(value)}`, + ); + const data = array.find((item) => item[key] === value); + return data; } diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index 824557ad..178ea705 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -6,94 +6,94 @@ import type { Plugin } from "~/typings/plugin"; // See https://outline.itsnik.de/s/dockstat/doc/plugin-development-3UBj9gNMKF for more info const ExamplePlugin: Plugin = { - name: "Example Plugin", - version: "1.0.0", - - async onContainerStart(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} started on ${containerInfo.hostId}` - ); - }, - - async onContainerStop(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} stopped on ${containerInfo.hostId}` - ); - }, - - async onContainerExit(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} exited on ${containerInfo.hostId}` - ); - }, - - async onContainerCreate(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} created on ${containerInfo.hostId}` - ); - }, - - async onContainerDestroy(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}` - ); - }, - - async onContainerPause(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} pause on ${containerInfo.hostId}` - ); - }, - - async onContainerUnpause(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} resumed on ${containerInfo.hostId}` - ); - }, - - async onContainerRestart(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} restarted on ${containerInfo.hostId}` - ); - }, - - async onContainerUpdate(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} updated on ${containerInfo.hostId}` - ); - }, - - async onContainerRename(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} renamed on ${containerInfo.hostId}` - ); - }, - - async onContainerHealthStatus(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} changed status to ${containerInfo.status}` - ); - }, - - async onHostUnreachable(host: string, err: string) { - logger.info(`Server ${host} unreachable - ${err}`); - }, - - async onHostReachableAgain(host: string) { - logger.info(`Server ${host} reachable`); - }, - - async handleContainerDie(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} died on ${containerInfo.hostId}` - ); - }, - - async onContainerKill(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} killed on ${containerInfo.hostId}` - ); - }, + name: "Example Plugin", + version: "1.0.0", + + async onContainerStart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} started on ${containerInfo.hostId}`, + ); + }, + + async onContainerStop(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} stopped on ${containerInfo.hostId}`, + ); + }, + + async onContainerExit(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} exited on ${containerInfo.hostId}`, + ); + }, + + async onContainerCreate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} created on ${containerInfo.hostId}`, + ); + }, + + async onContainerDestroy(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}`, + ); + }, + + async onContainerPause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} pause on ${containerInfo.hostId}`, + ); + }, + + async onContainerUnpause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} resumed on ${containerInfo.hostId}`, + ); + }, + + async onContainerRestart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} restarted on ${containerInfo.hostId}`, + ); + }, + + async onContainerUpdate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} updated on ${containerInfo.hostId}`, + ); + }, + + async onContainerRename(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} renamed on ${containerInfo.hostId}`, + ); + }, + + async onContainerHealthStatus(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} changed status to ${containerInfo.status}`, + ); + }, + + async onHostUnreachable(host: string, err: string) { + logger.info(`Server ${host} unreachable - ${err}`); + }, + + async onHostReachableAgain(host: string) { + logger.info(`Server ${host} reachable`); + }, + + async handleContainerDie(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} died on ${containerInfo.hostId}`, + ); + }, + + async onContainerKill(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} killed on ${containerInfo.hostId}`, + ); + }, } satisfies Plugin; export default ExamplePlugin; diff --git a/src/plugins/telegram.plugin.ts b/src/plugins/telegram.plugin.ts index 5a938e71..7e43f1a5 100644 --- a/src/plugins/telegram.plugin.ts +++ b/src/plugins/telegram.plugin.ts @@ -7,31 +7,31 @@ const TELEGRAM_BOT_TOKEN = "CHANGE_ME"; // Replace with your bot token const TELEGRAM_CHAT_ID = "CHANGE_ME"; // Replace with your chat ID const TelegramNotificationPlugin: Plugin = { - name: "Telegram Notification Plugin", - version: "1.0.0", + name: "Telegram Notification Plugin", + version: "1.0.0", - async onContainerStart(containerInfo: ContainerInfo) { - const message = `Container Started: ${containerInfo.name} on ${containerInfo.hostId}`; - try { - const response = await fetch( - `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - chat_id: TELEGRAM_CHAT_ID, - text: message, - }), - } - ); - if (!response.ok) { - logger.error(`HTTP error ${response.status}`); - } - logger.info("Telegram notification sent."); - } catch (error) { - logger.error("Failed to send Telegram notification", error as string); - } - }, + async onContainerStart(containerInfo: ContainerInfo) { + const message = `Container Started: ${containerInfo.name} on ${containerInfo.hostId}`; + try { + const response = await fetch( + `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + chat_id: TELEGRAM_CHAT_ID, + text: message, + }), + }, + ); + if (!response.ok) { + logger.error(`HTTP error ${response.status}`); + } + logger.info("Telegram notification sent."); + } catch (error) { + logger.error("Failed to send Telegram notification", error as string); + } + }, } satisfies Plugin; export default TelegramNotificationPlugin; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 80c07b60..3513901d 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -5,582 +5,581 @@ import { backupDir } from "~/core/database/backup"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import { responseHandler } from "~/core/utils/response-handler"; import { hashApiKey } from "~/middleware/auth"; import type { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; + .get( + "", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - responses: { - "200": { - description: "Successfully retrieved configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - fetching_interval: { - type: "number", - example: 5, - }, - keep_data_for: { - type: "number", - example: 7, - }, - api_key: { - type: "string", - example: "hashed_api_key", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/plugins", - ({}) => { - try { - return pluginManager.getLoadedPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - responses: { - "200": { - description: "Successfully retrieved plugins", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-plugin", - }, - version: { - type: "string", - example: "1.0.0", - }, - status: { - type: "string", - example: "active", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving plugins", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting all registered plugins", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Returns current API configuration including data retention policies and security settings", + responses: { + "200": { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fetching_interval: { + type: "number", + example: 5, + }, + keep_data_for: { + type: "number", + example: 7, + }, + api_key: { + type: "string", + example: "hashed_api_key", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/plugins", + () => { + try { + return pluginManager.getLoadedPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + responses: { + "200": { + description: "Successfully retrieved plugins", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-plugin", + }, + version: { + type: "string", + example: "1.0.0", + }, + status: { + type: "string", + example: "active", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving plugins", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting all registered plugins", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { fetching_interval, keep_data_for, api_key } = body; - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key) - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - responses: { - "200": { - description: "Successfully updated configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated DockStatAPI config", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error updating the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - } - ) - .get( - "/package", - async () => { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key), + ); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", + responses: { + "200": { + description: "Successfully updated configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated DockStatAPI config", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error updating the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), + }, + ) + .get( + "/package", + async () => { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json` - ); + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, + ); - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - responses: { - "200": { - description: "Successfully retrieved package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error while reading package.json", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .post( - "/backup", - async ({ set }) => { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return responseHandler.ok(set, backupFilename); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Backs up the internal database", - responses: { - "200": { - description: "Successfully created backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "backup_2024-03-20_12-00-00.db.bak", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error creating backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error backing up", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/backup", - async ({ set }) => { - try { - const backupFiles = readdirSync(backupDir); + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Displays package metadata including dependencies, contributors, and licensing information", + responses: { + "200": { + description: "Successfully retrieved package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0", + }, + description: { + type: "string", + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", + }, + license: { + type: "string", + example: "CC BY-NC 4.0", + }, + authorName: { + type: "string", + example: "ItsNik", + }, + authorEmail: { + type: "string", + example: "info@itsnik.de", + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik", + }, + contributors: { + type: "array", + items: { + type: "string", + }, + example: [], + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + }, + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error while reading package.json", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .post( + "/backup", + async ({ set }) => { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return responseHandler.ok(set, backupFilename); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Backs up the internal database", + responses: { + "200": { + description: "Successfully created backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "backup_2024-03-20_12-00-00.db.bak", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error creating backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error backing up", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/backup", + async ({ set }) => { + try { + const backupFiles = readdirSync(backupDir); - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Lists all available backups", - responses: { - "200": { - description: "Successfully retrieved backup list", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "string", - }, - example: [ - "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak", - ], - }, - }, - }, - }, - "400": { - description: "Error retrieving backup list", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Reading Backup directory", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Lists all available backups", + responses: { + "200": { + description: "Successfully retrieved backup list", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "string", + }, + example: [ + "backup_2024-03-20_12-00-00.db.bak", + "backup_2024-03-19_12-00-00.db.bak", + ], + }, + }, + }, + }, + "400": { + description: "Error retrieving backup list", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Reading Backup directory", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) - .get( - "/backup/download", - async ({ query, set }) => { - try { - const filename = query.filename || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; + .get( + "/backup/download", + async ({ query, set }) => { + try { + const filename = query.filename || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } - set.headers["Content-Type"] = "application/octet-stream"; - set.headers[ - "Content-Disposition" - ] = `attachment; filename="${filename}"`; - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Download a specific backup or the latest if no filename is provided", - responses: { - "200": { - description: "Successfully downloaded backup file", - content: { - "application/octet-stream": { - schema: { - type: "string", - format: "binary", - example: "Binary backup file content", - }, - }, - }, - headers: { - "Content-Disposition": { - schema: { - type: "string", - example: - 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', - }, - }, - }, - }, - "400": { - description: "Error downloading backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Backup download failed", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - filename: t.Optional(t.String()), - }), - } - ) - .post( - "/restore", - async ({ body, set }) => { - try { - const { file } = body; + set.headers["Content-Type"] = "application/octet-stream"; + set.headers["Content-Disposition"] = + `attachment; filename="${filename}"`; + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Download a specific backup or the latest if no filename is provided", + responses: { + "200": { + description: "Successfully downloaded backup file", + content: { + "application/octet-stream": { + schema: { + type: "string", + format: "binary", + example: "Binary backup file content", + }, + }, + }, + headers: { + "Content-Disposition": { + schema: { + type: "string", + example: + 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', + }, + }, + }, + }, + "400": { + description: "Error downloading backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Backup download failed", + }, + }, + }, + }, + }, + }, + }, + }, + query: t.Object({ + filename: t.Optional(t.String()), + }), + }, + ) + .post( + "/restore", + async ({ body, set }) => { + try { + const { file } = body; - set.headers["Content-Type"] = "text/html"; + set.headers["Content-Type"] = "text/html"; - if (!file) { - throw new Error("No file uploaded"); - } + if (!file) { + throw new Error("No file uploaded"); + } - if (!file.name.endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } + if (!file.name.endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); - return responseHandler.ok(set, "Database restored successfully"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - body: t.Object({ file: t.File() }), - detail: { - tags: ["Management"], - description: "Restore database from uploaded backup file", - responses: { - "200": { - description: "Successfully restored database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Database restored successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restoring database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Database restoration error", - }, - }, - }, - }, - }, - }, - }, - }, - } - ); + return responseHandler.ok(set, "Database restored successfully"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + body: t.Object({ file: t.File() }), + detail: { + tags: ["Management"], + description: "Restore database from uploaded backup file", + responses: { + "200": { + description: "Successfully restored database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Database restored successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error restoring database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Database restoration error", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ); diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts index 09df03a7..83d31c99 100644 --- a/src/routes/docker-websocket.ts +++ b/src/routes/docker-websocket.ts @@ -6,8 +6,8 @@ import split2 from "split2"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; @@ -15,122 +15,122 @@ import { responseHandler } from "~/core/utils/response-handler"; //biome-ignore lint/suspicious/noExplicitAny: const activeDockerConnections = new Set>(); const connectionStreams = new Map< - //biome-ignore lint/suspicious/noExplicitAny: - ElysiaWS, - Array<{ statsStream: Readable; splitStream: ReturnType }> + //biome-ignore lint/suspicious/noExplicitAny: + ElysiaWS, + Array<{ statsStream: Readable; splitStream: ReturnType }> >(); export const dockerWebsocketRoutes = new Elysia({ prefix: "/ws" }).ws( - "/docker", - { - async open(ws) { - activeDockerConnections.add(ws); - connectionStreams.set(ws, []); + "/docker", + { + async open(ws) { + activeDockerConnections.add(ws); + connectionStreams.set(ws, []); - ws.send(JSON.stringify({ message: "Connection established" })); - logger.info(`New Docker WebSocket established (${ws.id})`); + ws.send(JSON.stringify({ message: "Connection established" })); + logger.info(`New Docker WebSocket established (${ws.id})`); - try { - const hosts = dbFunctions.getDockerHosts(); - logger.debug(`Retrieved ${hosts.length} docker host(s)`); + try { + const hosts = dbFunctions.getDockerHosts(); + logger.debug(`Retrieved ${hosts.length} docker host(s)`); - for (const host of hosts) { - if (ws.readyState !== 1) { - break; - } + for (const host of hosts) { + if (ws.readyState !== 1) { + break; + } - const docker = getDockerClient(host); - await docker.ping(); - const containers = await docker.listContainers({ all: true }); - logger.debug( - `Found ${containers.length} containers on ${host.name} (id: ${host.id})` - ); + const docker = getDockerClient(host); + await docker.ping(); + const containers = await docker.listContainers({ all: true }); + logger.debug( + `Found ${containers.length} containers on ${host.name} (id: ${host.id})`, + ); - for (const containerInfo of containers) { - if (ws.readyState !== 1) { - break; - } + for (const containerInfo of containers) { + if (ws.readyState !== 1) { + break; + } - const container = docker.getContainer(containerInfo.Id); - const statsStream = (await container.stats({ - stream: true, - })) as Readable; - const splitStream = split2(); + const container = docker.getContainer(containerInfo.Id); + const statsStream = (await container.stats({ + stream: true, + })) as Readable; + const splitStream = split2(); - connectionStreams.get(ws)?.push({ statsStream, splitStream }); + connectionStreams.get(ws)?.push({ statsStream, splitStream }); - statsStream - .on("close", () => splitStream.destroy()) - .pipe(splitStream) - .on("data", (line: string) => { - if (ws.readyState !== 1 || !line) { - return; - } - try { - const stats = JSON.parse(line); - ws.send( - JSON.stringify({ - id: containerInfo.Id, - hostId: host.id, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats) || 0, - memoryUsage: calculateMemoryUsage(stats) || 0, - }) - ); - } catch (error) { - logger.error(`Parse error: ${error}`); - } - }) - .on("error", (error: Error) => { - logger.error(`Stream error: ${error}`); - statsStream.destroy(); - ws.send( - JSON.stringify({ - hostId: host.name, - containerId: containerInfo.Id, - error: `Stats stream error: ${error}`, - }) - ); - }); - } - } - } catch (error) { - logger.error(`Connection error: ${error}`); - ws.send( - JSON.stringify( - responseHandler.error( - { headers: {} }, - error as string, - "Docker connection failed", - 500 - ) - ) - ); - } - }, + statsStream + .on("close", () => splitStream.destroy()) + .pipe(splitStream) + .on("data", (line: string) => { + if (ws.readyState !== 1 || !line) { + return; + } + try { + const stats = JSON.parse(line); + ws.send( + JSON.stringify({ + id: containerInfo.Id, + hostId: host.id, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats) || 0, + memoryUsage: calculateMemoryUsage(stats) || 0, + }), + ); + } catch (error) { + logger.error(`Parse error: ${error}`); + } + }) + .on("error", (error: Error) => { + logger.error(`Stream error: ${error}`); + statsStream.destroy(); + ws.send( + JSON.stringify({ + hostId: host.name, + containerId: containerInfo.Id, + error: `Stats stream error: ${error}`, + }), + ); + }); + } + } + } catch (error) { + logger.error(`Connection error: ${error}`); + ws.send( + JSON.stringify( + responseHandler.error( + { headers: {} }, + error as string, + "Docker connection failed", + 500, + ), + ), + ); + } + }, - message(ws, message) { - if (message === "pong") ws.pong(); - }, + message(ws, message) { + if (message === "pong") ws.pong(); + }, - close(ws) { - logger.info(`Closing connection ${ws.id}`); - activeDockerConnections.delete(ws); + close(ws) { + logger.info(`Closing connection ${ws.id}`); + activeDockerConnections.delete(ws); - const streams = connectionStreams.get(ws) || []; - for (const { statsStream, splitStream } of streams) { - try { - statsStream.unpipe(splitStream); - statsStream.destroy(); - splitStream.destroy(); - } catch (error) { - logger.error(`Cleanup error: ${error}`); - } - } - connectionStreams.delete(ws); - }, - } + const streams = connectionStreams.get(ws) || []; + for (const { statsStream, splitStream } of streams) { + try { + statsStream.unpipe(splitStream); + statsStream.destroy(); + splitStream.destroy(); + } catch (error) { + logger.error(`Cleanup error: ${error}`); + } + } + connectionStreams.delete(ws); + }, + }, ); diff --git a/src/routes/live-logs.ts b/src/routes/live-logs.ts index 5c451fcf..1b7fbfd8 100644 --- a/src/routes/live-logs.ts +++ b/src/routes/live-logs.ts @@ -9,30 +9,30 @@ import type { log_message } from "~/typings/database"; const activeConnections = new Set>(); export const liveLogs = new Elysia({ prefix: "/ws" }).ws("/logs", { - open(ws) { - activeConnections.add(ws); - ws.send({ - message: "Connection established", - level: "info", - timestamp: new Date().toISOString(), - file: "live-logs.ts", - line: 14, - }); - logger.info(`New Logs WebSocket established (${ws.id})`); - }, - close(ws) { - logger.info(`Logs WebSocket closed (${ws.id})`); - activeConnections.delete(ws); - }, + open(ws) { + activeConnections.add(ws); + ws.send({ + message: "Connection established", + level: "info", + timestamp: new Date().toISOString(), + file: "live-logs.ts", + line: 14, + }); + logger.info(`New Logs WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Logs WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, }); export function logToClients(data: log_message) { - for (const ws of activeConnections) { - try { - ws.send(JSON.stringify(data)); - } catch (error) { - activeConnections.delete(ws); - logger.error("Failed to send to WebSocket:", error); - } - } + for (const ws of activeConnections) { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + } } diff --git a/src/routes/live-stacks.ts b/src/routes/live-stacks.ts index a841678e..b093fd21 100644 --- a/src/routes/live-stacks.ts +++ b/src/routes/live-stacks.ts @@ -7,24 +7,24 @@ import type { stackSocketMessage } from "~/typings/websocket"; const activeConnections = new Set>(); export const liveStacks = new Elysia({ prefix: "/ws" }).ws("/stacks", { - open(ws) { - activeConnections.add(ws); - ws.send({ message: "Connection established" }); - logger.info(`New Stacks WebSocket established (${ws.id})`); - }, - close(ws) { - logger.info(`Stacks WebSocket closed (${ws.id})`); - activeConnections.delete(ws); - }, + open(ws) { + activeConnections.add(ws); + ws.send({ message: "Connection established" }); + logger.info(`New Stacks WebSocket established (${ws.id})`); + }, + close(ws) { + logger.info(`Stacks WebSocket closed (${ws.id})`); + activeConnections.delete(ws); + }, }); export function postToClient(data: stackSocketMessage) { - for (const ws of activeConnections) { - try { - ws.send(JSON.stringify(data)); - } catch (error) { - activeConnections.delete(ws); - logger.error("Failed to send to WebSocket:", error); - } - } + for (const ws of activeConnections) { + try { + ws.send(JSON.stringify(data)); + } catch (error) { + activeConnections.delete(ws); + logger.error("Failed to send to WebSocket:", error); + } + } } diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts index b504e4d8..d81d24d6 100644 --- a/src/routes/stacks.ts +++ b/src/routes/stacks.ts @@ -1,598 +1,598 @@ import { Elysia, t } from "elysia"; import { dbFunctions } from "~/core/database"; import { - deployStack, - getAllStacksStatus, - getStackStatus, - pullStackImages, - removeStack, - restartStack, - startStack, - stopStack, + deployStack, + getAllStacksStatus, + getStackStatus, + pullStackImages, + removeStack, + restartStack, + startStack, + stopStack, } from "~/core/stacks/controller"; import { logger } from "~/core/utils/logger"; import { responseHandler } from "~/core/utils/response-handler"; import type { stacks_config } from "~/typings/database"; export const stackRoutes = new Elysia({ prefix: "/stacks" }) - .post( - "/deploy", - async ({ set, body }) => { - try { - await deployStack(body as stacks_config); - logger.info(`Deployed Stack (${body.name})`); - return responseHandler.ok( - set, - `Stack ${body.name} deployed successfully` - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + .post( + "/deploy", + async ({ set, body }) => { + try { + await deployStack(body as stacks_config); + logger.info(`Deployed Stack (${body.name})`); + return responseHandler.ok( + set, + `Stack ${body.name} deployed successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error( - set, - errorMsg, - "Error deploying stack, please check the server logs for more information" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Deploys a new Docker stack using a provided compose specification, allowing custom configurations and image updates", - responses: { - "200": { - description: "Successfully deployed stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack example-stack deployed successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error deploying stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error deploying stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - name: t.String(), - version: t.Number(), - custom: t.Boolean(), - source: t.String(), - compose_spec: t.Any(), - }), - } - ) - .post( - "/start", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack ID needed"); - } - await startStack(body.stackId); - logger.info(`Started Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} started successfully` - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + return responseHandler.error( + set, + errorMsg, + "Error deploying stack, please check the server logs for more information", + ); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Deploys a new Docker stack using a provided compose specification, allowing custom configurations and image updates", + responses: { + "200": { + description: "Successfully deployed stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack example-stack deployed successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error deploying stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error deploying stack", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + name: t.String(), + version: t.Number(), + custom: t.Boolean(), + source: t.String(), + compose_spec: t.Any(), + }), + }, + ) + .post( + "/start", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack ID needed"); + } + await startStack(body.stackId); + logger.info(`Started Stack (${body.stackId})`); + return responseHandler.ok( + set, + `Stack ${body.stackId} started successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error(set, errorMsg, "Error starting stack"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Initiates a Docker stack, starting all associated containers", - responses: { - "200": { - description: "Successfully started stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 started successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error starting stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error starting stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ) - .post( - "/stop", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await stopStack(body.stackId); - logger.info(`Stopped Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} stopped successfully` - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + return responseHandler.error(set, errorMsg, "Error starting stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Initiates a Docker stack, starting all associated containers", + responses: { + "200": { + description: "Successfully started stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 started successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error starting stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error starting stack", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ) + .post( + "/stop", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack needed"); + } + await stopStack(body.stackId); + logger.info(`Stopped Stack (${body.stackId})`); + return responseHandler.ok( + set, + `Stack ${body.stackId} stopped successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error(set, errorMsg, "Error stopping stack"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Halts a running Docker stack and its containers while preserving configurations", - responses: { - "200": { - description: "Successfully stopped stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 stopped successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error stopping stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error stopping stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ) - .post( - "/restart", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await restartStack(body.stackId); - logger.info(`Restarted Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} restarted successfully` - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + return responseHandler.error(set, errorMsg, "Error stopping stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Halts a running Docker stack and its containers while preserving configurations", + responses: { + "200": { + description: "Successfully stopped stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 stopped successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error stopping stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error stopping stack", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ) + .post( + "/restart", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack needed"); + } + await restartStack(body.stackId); + logger.info(`Restarted Stack (${body.stackId})`); + return responseHandler.ok( + set, + `Stack ${body.stackId} restarted successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error(set, errorMsg, "Error restarting stack"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Performs full stack restart - stops and restarts all stack components in sequence", - responses: { - "200": { - description: "Successfully restarted stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 restarted successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restarting stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error restarting stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ) - .post( - "/pull-images", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await pullStackImages(body.stackId); - logger.info(`Pulled Stack images (${body.stackId})`); - return responseHandler.ok( - set, - `Images for stack ${body.stackId} pulled successfully` - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + return responseHandler.error(set, errorMsg, "Error restarting stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Performs full stack restart - stops and restarts all stack components in sequence", + responses: { + "200": { + description: "Successfully restarted stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 restarted successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error restarting stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error restarting stack", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ) + .post( + "/pull-images", + async ({ set, body }) => { + try { + if (!body.stackId) { + throw new Error("Stack needed"); + } + await pullStackImages(body.stackId); + logger.info(`Pulled Stack images (${body.stackId})`); + return responseHandler.ok( + set, + `Images for stack ${body.stackId} pulled successfully`, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error(set, errorMsg, "Error pulling images"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Updates container images for a stack using Docker's pull mechanism (requires stack ID)", - responses: { - "200": { - description: "Successfully pulled images", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Images for stack 1 pulled successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error pulling images", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error pulling images", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ) - .get( - "/status", - async ({ set, query }) => { - try { - // biome-ignore lint/suspicious/noExplicitAny: - let status: Record; - let res = {}; + return responseHandler.error(set, errorMsg, "Error pulling images"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Updates container images for a stack using Docker's pull mechanism (requires stack ID)", + responses: { + "200": { + description: "Successfully pulled images", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Images for stack 1 pulled successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error pulling images", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error pulling images", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ) + .get( + "/status", + async ({ set, query }) => { + try { + // biome-ignore lint/suspicious/noExplicitAny: + let status: Record; + let res = {}; - logger.debug("Entering stack status handler"); - logger.debug(`Request body: ${JSON.stringify(query)}`); + logger.debug("Entering stack status handler"); + logger.debug(`Request body: ${JSON.stringify(query)}`); - if (query.stackId !== 0) { - logger.debug(`Fetching status for stackId=${query.stackId}`); - status = await getStackStatus(query.stackId); - logger.debug( - `Retrieved status for stackId=${query.stackId}: ${JSON.stringify( - status - )}` - ); + if (query.stackId !== 0) { + logger.debug(`Fetching status for stackId=${query.stackId}`); + status = await getStackStatus(query.stackId); + logger.debug( + `Retrieved status for stackId=${query.stackId}: ${JSON.stringify( + status, + )}`, + ); - res = responseHandler.ok( - set, - `Stack ${query.stackId} status retrieved successfully` - ); - logger.info("Fetched Stack status"); - } else { - logger.debug("Fetching status for all stacks"); - status = await getAllStacksStatus(); - logger.debug( - `Retrieved status for all stacks: ${JSON.stringify(status)}` - ); + res = responseHandler.ok( + set, + `Stack ${query.stackId} status retrieved successfully`, + ); + logger.info("Fetched Stack status"); + } else { + logger.debug("Fetching status for all stacks"); + status = await getAllStacksStatus(); + logger.debug( + `Retrieved status for all stacks: ${JSON.stringify(status)}`, + ); - res = responseHandler.ok(set, "Fetched all Stack's status"); - logger.info("Fetched all Stack status"); - } + res = responseHandler.ok(set, "Fetched all Stack's status"); + logger.info("Fetched all Stack status"); + } - logger.debug("Returning response with status data"); - return { ...res, status: status }; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.debug(`Error occurred while fetching stack status: ${errorMsg}`); + logger.debug("Returning response with status data"); + return { ...res, status: status }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.debug(`Error occurred while fetching stack status: ${errorMsg}`); - return responseHandler.error( - set, - errorMsg, - "Error getting stack status" - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Retrieves operational status for either a specific stack (by ID) or all managed stacks (ID: 0)", - responses: { - "200": { - description: "Successfully retrieved stack status", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 status retrieved successfully", - }, - status: { - type: "object", - properties: { - name: { - type: "string", - example: "example-stack", - }, - status: { - type: "string", - example: "running", - }, - containers: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-stack_web_1", - }, - status: { - type: "string", - example: "running", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error getting stack status", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting stack status", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - stackId: t.Number(), - }), - } - ) - .get( - "/", - async ({ set }) => { - try { - const stacks = dbFunctions.getStacks(); - logger.info("Fetched Stacks"); - return stacks; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + return responseHandler.error( + set, + errorMsg, + "Error getting stack status", + ); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Retrieves operational status for either a specific stack (by ID) or all managed stacks (ID: 0)", + responses: { + "200": { + description: "Successfully retrieved stack status", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 status retrieved successfully", + }, + status: { + type: "object", + properties: { + name: { + type: "string", + example: "example-stack", + }, + status: { + type: "string", + example: "running", + }, + containers: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-stack_web_1", + }, + status: { + type: "string", + example: "running", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error getting stack status", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting stack status", + }, + }, + }, + }, + }, + }, + }, + }, + query: t.Object({ + stackId: t.Number(), + }), + }, + ) + .get( + "/", + async ({ set }) => { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks"); + return stacks; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error(set, errorMsg, "Error getting stacks"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Lists all registered stacks with their complete configuration details", - responses: { - "200": { - description: "Successfully retrieved stacks", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "number", - example: 1, - }, - name: { - type: "string", - example: "example-stack", - }, - version: { - type: "number", - example: 1, - }, - source: { - type: "string", - example: "github.com/example/repo", - }, - automatic_reboot_on_error: { - type: "boolean", - example: true, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error getting stacks", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting stacks", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .delete( - "/", - async ({ set, body }) => { - try { - const { stackId } = body; - await removeStack(stackId); - logger.info(`Deleted Stack ${stackId}`); - return responseHandler.ok(set, `Stack ${stackId} deleted successfully`); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); + return responseHandler.error(set, errorMsg, "Error getting stacks"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Lists all registered stacks with their complete configuration details", + responses: { + "200": { + description: "Successfully retrieved stacks", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "number", + example: 1, + }, + name: { + type: "string", + example: "example-stack", + }, + version: { + type: "number", + example: 1, + }, + source: { + type: "string", + example: "github.com/example/repo", + }, + automatic_reboot_on_error: { + type: "boolean", + example: true, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error getting stacks", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting stacks", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .delete( + "/", + async ({ set, body }) => { + try { + const { stackId } = body; + await removeStack(stackId); + logger.info(`Deleted Stack ${stackId}`); + return responseHandler.ok(set, `Stack ${stackId} deleted successfully`); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); - return responseHandler.error(set, errorMsg, "Error deleting stack"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Permanently removes a stack configuration and cleans up associated resources", - responses: { - "200": { - description: "Successfully deleted stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 deleted successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error deleting stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error deleting stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - } - ); + return responseHandler.error(set, errorMsg, "Error deleting stack"); + } + }, + { + detail: { + tags: ["Stacks"], + description: + "Permanently removes a stack configuration and cleans up associated resources", + responses: { + "200": { + description: "Successfully deleted stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Stack 1 deleted successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error deleting stack", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error deleting stack", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + stackId: t.Number(), + }), + }, + ); From 871ab4f92dee49569d9028490442b2537d265430 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Mon, 2 Jun 2025 15:24:34 +0200 Subject: [PATCH 327/369] Feat: Stack validation --- .github/workflows/ci.yml | 2 +- package.json | 2 ++ src/core/database/database.ts | 3 ++- src/core/database/stacks.ts | 39 +++++++++++++++++++++-------- src/core/stacks/checker.ts | 47 +++++++++++++++++++++++++++++++++++ src/core/stacks/controller.ts | 21 +++------------- src/index.ts | 3 +++ 7 files changed, 88 insertions(+), 29 deletions(-) create mode 100644 src/core/stacks/checker.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87552540..77cfedbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - name: Run unit tests run: | export PAD_NEW_LINES=false - docker compose -f docker/docker-compose.unit-test.yaml up -d + docker compose -f docker/docker-compose.dev.yaml up -d bun test - name: Log unit test files diff --git a/package.json b/package.json index 5b696e96..43b07677 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "dockerode": "^4.0.6", "elysia": "latest", "elysia-remote-dts": "^1.0.3", + "js-yaml": "^4.1.0", "knip": "latest", "logestic": "^1.2.4", "split2": "^4.2.0", @@ -43,6 +44,7 @@ "@biomejs/biome": "1.9.4", "@types/bun": "latest", "@types/dockerode": "^3.3.39", + "@types/js-yaml": "^4.0.9", "@types/node": "^22.15.29", "@types/split2": "^4.2.3", "bun-types": "latest", diff --git a/src/core/database/database.ts b/src/core/database/database.ts index f8de7cb1..3ee6f54f 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -47,7 +47,8 @@ export function init() { version INTEGER NOT NULL, custom BOOLEAN NOT NULL, source TEXT NOT NULL, - compose_spec TEXT NOT NULL + compose_spec TEXT NOT NULL, + status TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS docker_hosts ( diff --git a/src/core/database/stacks.ts b/src/core/database/stacks.ts index e872bbcf..c160a982 100644 --- a/src/core/database/stacks.ts +++ b/src/core/database/stacks.ts @@ -1,5 +1,4 @@ import type { stacks_config } from "~/typings/database"; -import type { Stack } from "~/typings/docker-compose"; import { findObjectByKey } from "../utils/helpers"; import { db } from "./database"; import { executeDbOperation } from "./helper"; @@ -7,19 +6,24 @@ import { executeDbOperation } from "./helper"; const stmt = { insert: db.prepare(` INSERT INTO stacks_config ( - name, version, custom, source, compose_spec - ) VALUES (?, ?, ?, ?, ?) + name, version, custom, source, compose_spec, status + ) VALUES (?, ?, ?, ?, ?, ?) `), selectAll: db.prepare(` - SELECT id, name, version, custom, source, compose_spec + SELECT id, name, version, custom, source, compose_spec, status FROM stacks_config - ORDER BY name DESC + ORDER BY id DESC `), update: db.prepare(` UPDATE stacks_config SET name = ?, custom = ?, source = ?, compose_spec = ? - WHERE name = ? + WHERE id = ? `), + setStatus: db.prepare(` + UPDATE stacks_config + SET status = ? + WHERE id = ? + `), delete: db.prepare("DELETE FROM stacks_config WHERE id = ?"), }; @@ -31,6 +35,7 @@ export function addStack(stack: stacks_config) { stack.custom, stack.source, stack.compose_spec, + "active", ), ); @@ -40,7 +45,7 @@ export function addStack(stack: stacks_config) { export function getStacks() { return executeDbOperation("Get Stacks", () => stmt.selectAll.all(), - ) as Stack[]; + ) as stacks_config[]; } export function deleteStack(id: number) { @@ -54,13 +59,27 @@ export function deleteStack(id: number) { } export function updateStack(stack: stacks_config) { - return executeDbOperation("Update Stack", () => + return executeDbOperation("Update Stack", () => { + if (!stack.id) { + throw new Error("Stack ID needed"); + } stmt.update.run( + stack.id, stack.version, stack.custom, stack.source, stack.name, stack.compose_spec, - ), - ); + ); + }); +} + +export function setStackStatus( + stack: stacks_config, + status: "active" | "error" = "active", +) { + if (!stack.id) { + throw new Error("Stack ID needed"); + } + stmt.setStatus.run(status, stack.id); } diff --git a/src/core/stacks/checker.ts b/src/core/stacks/checker.ts new file mode 100644 index 00000000..8d6052e7 --- /dev/null +++ b/src/core/stacks/checker.ts @@ -0,0 +1,47 @@ +import yaml from "js-yaml"; +import { dbFunctions } from "../database"; +import { logger } from "../utils/logger"; + +const stacks = dbFunctions.getStacks(); + +export async function checkStacks() { + logger.debug(`Checking ${stacks.length} stack(s)`); + for (const stack of stacks) { + try { + logger.debug(`Checking ${stack.id}`); + const composeFile = Bun.file( + `stacks/${stack.id}-${stack.name}/docker-compose.yaml`, + ); + + if (!(await composeFile.exists())) { + logger.error(`Stack (${stack.id} - ${stack.name}) has no compose file`); + dbFunctions.setStackStatus(stack, "error"); + continue; + } + + if ( + stack.compose_spec !== + JSON.stringify(yaml.load(await composeFile.text())) + ) { + logger.error( + `Stack (${stack.id} - ${stack.name}) does not match the saved compose file`, + ); + logger.debug(`Database config: ${stack.compose_spec}`); + logger.debug( + `Compose config: ${JSON.stringify( + yaml.load(await composeFile.text()), + )}`, + ); + dbFunctions.setStackStatus(stack, "error"); + continue; + } + + dbFunctions.setStackStatus(stack, "active"); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + } + } + + logger.info("Checked stacks"); +} diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 637a7e5d..8dd58612 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -7,10 +7,11 @@ import { postToClient } from "~/routes/live-stacks"; import type { stacks_config } from "~/typings/database"; import type { ComposeSpec, Stack } from "~/typings/docker-compose"; import { findObjectByKey } from "../utils/helpers"; +import { checkStacks } from "./checker"; const wrapProgressCallback = (progressCallback?: (log: string) => void) => { return progressCallback - ? (chunk: Buffer, streamSource?: "stdout" | "stderr") => { + ? (chunk: Buffer) => { const log = chunk.toString(); progressCallback(log); } @@ -197,6 +198,8 @@ export async function deployStack(stack_config: stacks_config): Promise { }, }); throw new Error(errorMsg); + } finally { + await checkStacks(); } } @@ -399,19 +402,3 @@ export async function getAllStacksStatus(): Promise { throw new Error(errorMsg); } } - -async function backupStack(stackId: number) { - if (!stackId) { - throw new Error("No Stack ID provided"); - } - - const stacks = dbFunctions.getStacks(); - - const stack = findObjectByKey(stacks, "id", stackId); - - if (!stack) { - throw new Error(`No Stack with Id: ${stackId} found`); - } - - const stack_path = `${stack.id}-${stack.name}`; -} diff --git a/src/index.ts b/src/index.ts index e52e8a71..c8189133 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,7 @@ import { liveLogs } from "~/routes/live-logs"; import { backendLogs } from "~/routes/logs"; import { stackRoutes } from "~/routes/stacks"; import type { config } from "~/typings/database"; +import { checkStacks } from "./core/stacks/checker"; import { liveStacks } from "./routes/live-stacks"; console.log(""); @@ -168,6 +169,8 @@ const initializeServer = async () => { ); } + await checkStacks(); + logger.info("Started server"); console.log("----- [ ############## ]"); } catch (error) { From e0ed2557ecfdc546786fb4969e8dec602a35708b Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 6 Jun 2025 15:04:14 +0200 Subject: [PATCH 328/369] Chore: Refactor Stacks controller and stuff --- .github/workflows/ci.yml | 2 +- package.json | 2 + src/core/stacks/controller.ts | 574 ++++++------------ src/core/stacks/operations/runStackCommand.ts | 89 +++ src/core/stacks/operations/stackHelpers.ts | 35 ++ src/core/stacks/operations/stackStatus.ts | 89 +++ src/index.ts | 292 ++++----- src/routes/api-config.ts | 3 +- tsconfig.json | 200 +++--- 9 files changed, 660 insertions(+), 626 deletions(-) create mode 100644 src/core/stacks/operations/runStackCommand.ts create mode 100644 src/core/stacks/operations/stackHelpers.ts create mode 100644 src/core/stacks/operations/stackStatus.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77cfedbc..87552540 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - name: Run unit tests run: | export PAD_NEW_LINES=false - docker compose -f docker/docker-compose.dev.yaml up -d + docker compose -f docker/docker-compose.unit-test.yaml up -d bun test - name: Log unit test files diff --git a/package.json b/package.json index 43b07677..a3828ff0 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src" }, "dependencies": { + "@elysiajs/cors": "^1.3.3", + "@elysiajs/html": "^1.3.0", "@elysiajs/server-timing": "^1.3.0", "@elysiajs/static": "^1.3.0", "@elysiajs/swagger": "^1.3.0", diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 8dd58612..0899dd1f 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -1,404 +1,218 @@ -import { rm } from "node:fs/promises"; -import DockerCompose from "docker-compose"; -import YAML from "yaml"; +import { runStackCommand } from "./operations/runStackCommand"; +import { + getStackPath, + createStackYAML, + getStackName, +} from "./operations/stackHelpers"; import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { postToClient } from "~/routes/live-stacks"; import type { stacks_config } from "~/typings/database"; -import type { ComposeSpec, Stack } from "~/typings/docker-compose"; -import { findObjectByKey } from "../utils/helpers"; import { checkStacks } from "./checker"; - -const wrapProgressCallback = (progressCallback?: (log: string) => void) => { - return progressCallback - ? (chunk: Buffer) => { - const log = chunk.toString(); - progressCallback(log); - } - : undefined; -}; - -async function getStackName(stack_id: number): Promise { - logger.debug(`Fetching stack name for id ${stack_id}`); - const stacks = dbFunctions.getStacks(); - const stack = findObjectByKey(stacks, "id", stack_id); - if (!stack) { - throw new Error(`Stack with id ${stack_id} not found`); - } - return stack.name; -} - -async function runStackCommand( - stack_id: number, - command: ( - cwd: string, - progressCallback?: (log: string) => void, - ) => Promise, - action: string, -): Promise { - try { - logger.debug( - `Starting runStackCommand for stack_id=${stack_id}, action="${action}"`, - ); - - const stackName = await getStackName(stack_id); - logger.debug( - `Retrieved stack name "${stackName}" for stack_id=${stack_id}`, - ); - - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); - - const progressCallback = (log: string) => { - const message = log.trim(); - logger.debug( - `Progress for stack_id=${stack_id}, action="${action}": ${message}`, - ); - - // ERROR HANDLING FOR COMPOSE ACTIONS - if (message.includes("Error response from daemon")) { - logger.error( - `Error response from daemon: ${ - message.split("Error response from daemon:")[1] - }`, - ); - } - - postToClient({ - type: "stack-progress", - data: { - stack_id, - action, - message, - timestamp: new Date().toISOString(), - }, - }); - }; - - logger.debug( - `Executing command for stack_id=${stack_id}, action="${action}"`, - ); - const result = await command(stackPath, progressCallback); - logger.debug( - `Successfully completed command for stack_id=${stack_id}, action="${action}"`, - ); - - return result; - } catch (error) { - logger.debug( - `Error occurred for stack_id=${stack_id}, action="${action}": ${String( - error, - )}`, - ); - postToClient({ - type: "stack-error", - data: { - stack_id, - action, - message: String(error), - timestamp: new Date().toISOString(), - }, - }); - throw new Error( - `Error while ${action} stack "${stack_id}": ${String(error)}`, - ); - } -} - -async function getStackPath(stack: Stack): Promise { - const stackName = stack.name.trim().replace(/\s+/g, "_"); - const stackId = stack.id; - - if (!stackId) { - logger.error("Stack could not be parsed"); - throw new Error("Stack could not be parsed"); - } - - return `stacks/${stackId}-${stackName}`; -} - -async function createStackYAML(compose_spec: Stack): Promise { - const yaml = YAML.stringify(compose_spec.compose_spec); - const stackPath = await getStackPath(compose_spec); - await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { - createPath: true, - }); -} +import type { Stack } from "~/typings/docker-compose"; +import { wrapProgressCallback } from "./operations/runStackCommand"; +import type { ComposeSpec } from "~/typings/docker-compose"; +import { rm } from "node:fs/promises"; +import DockerCompose from "docker-compose"; export async function deployStack(stack_config: stacks_config): Promise { - try { - logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); - - if (!stack_config.name) { - throw new Error("Stack name needed"); - } - - const jsonStringStack = { - ...stack_config, - compose_spec: JSON.stringify(stack_config.compose_spec), - }; - - const stackId = dbFunctions.addStack(jsonStringStack); - - if (!stackId) { - throw new Error("Failed to add stack to database"); - } - - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "pending", - message: "Creating stack configuration", - }, - }); - - const stackYaml: Stack = { - id: stackId, - name: stack_config.name, - source: stack_config.source, - version: stack_config.version, - compose_spec: stack_config.compose_spec as unknown as ComposeSpec, // Weird stuff i am doing here... smh - }; - - await createStackYAML(stackYaml); - - await runStackCommand( - stackId, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "deploying", - ); - - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "deployed", - message: "Stack deployed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id: 0, - action: "deploying", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } finally { - await checkStacks(); - } + let stackId: number | null = null; + let stackPath = ""; + + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); + if (!stack_config.name) throw new Error("Stack name needed"); + + const jsonStringStack = { + ...stack_config, + compose_spec: JSON.stringify(stack_config.compose_spec), + }; + + stackId = dbFunctions.addStack(jsonStringStack) || null; + if (!stackId) { + throw new Error("Failed to add stack to database"); + } + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }); + + const stackYaml: Stack = { + id: stackId, + name: stack_config.name, + source: stack_config.source, + version: stack_config.version, + compose_spec: stack_config.compose_spec as unknown as ComposeSpec, + }; + + await createStackYAML(stackYaml); + stackPath = await getStackPath(stackYaml); + + await runStackCommand( + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying" + ); + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }); + + await checkStacks(); + } catch (error: unknown) { + const errorMsg = + error instanceof Error ? error.message : JSON.stringify(error); + logger.error(errorMsg); + if (stackId !== null) { + dbFunctions.deleteStack(stackId); + if (stackPath) { + try { + await rm(stackPath, { recursive: true }); + } catch (cleanupError) { + logger.error(`Error cleaning up stack path: ${cleanupError}`); + } + } + } + postToClient({ + type: "stack-error", + data: { + stack_id: stackId ?? 0, + action: "deploying", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } export async function stopStack(stack_id: number): Promise { - // Note the await to discard the result (convert to void) - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.downAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "stopping", - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.downAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping" + ); } export async function startStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "starting", - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting" + ); } export async function pullStackImages(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.pullAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "pulling-images", - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images" + ); } export async function restartStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.restartAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "restarting", - ); -} - -export async function getStackStatus( - stack_id: number, - //biome-ignore lint/suspicious/noExplicitAny: -): Promise> { - const status = await runStackCommand( - stack_id, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - //biome-ignore lint/suspicious/noExplicitAny: - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check", - ); - return status; + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting" + ); } export async function removeStack(stack_id: number): Promise { - try { - const _ = dbFunctions.deleteStack(stack_id); - - await runStackCommand( - stack_id, - async (cwd, progressCallback) => { - await DockerCompose.down({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }); - }, - "removing", - ); - - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - - try { - await rm(stackPath, { recursive: true }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } - - postToClient({ - type: "stack-removed", - data: { - stack_id, - message: "Stack removed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } -} - -interface DockerServiceStatus { - status: string; - ports: string[]; -} - -interface StackStatus { - services: Record; - healthy: number; - unhealthy: number; - total: number; + try { + const _ = dbFunctions.deleteStack(stack_id); + + await runStackCommand( + stack_id, + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }); + }, + "removing" + ); + + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + + try { + await rm(stackPath, { recursive: true }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } + + postToClient({ + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } -type StacksStatus = Record; - -export async function getAllStacksStatus(): Promise { - try { - const stacks = dbFunctions.getStacks(); - - const statusResults = await Promise.all( - stacks.map(async (stack) => { - const status = await runStackCommand( - stack.id as number, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - const services = rawStatus.data.services.reduce( - (acc: Record, service) => { - acc[service.name] = { - status: service.state, - ports: service.ports.map( - (port) => `${port.mapped?.address}:${port.mapped?.port}`, - ), - }; - return acc; - }, - {}, - ); - - const statusValues = Object.values(services); - return { - services, - healthy: statusValues.filter( - (s) => s.status === "running" || s.status.includes("Up"), - ).length, - unhealthy: statusValues.filter( - (s) => s.status !== "running" && !s.status.includes("Up"), - ).length, - total: statusValues.length, - }; - }, - "status-check", - ); - return { stackId: stack.id, status }; - }), - ); - - return statusResults.reduce((acc, { stackId, status }) => { - acc[String(stackId)] = status; - return acc; - }, {} as StacksStatus); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } -} +export { getStackStatus, getAllStacksStatus } from "./operations/stackStatus"; diff --git a/src/core/stacks/operations/runStackCommand.ts b/src/core/stacks/operations/runStackCommand.ts new file mode 100644 index 00000000..14191c60 --- /dev/null +++ b/src/core/stacks/operations/runStackCommand.ts @@ -0,0 +1,89 @@ +import { getStackName, getStackPath } from "./stackHelpers"; +import { postToClient } from "~/routes/live-stacks"; +import { logger } from "~/core/utils/logger"; +import type { Stack } from "~/typings/docker-compose"; + +export function wrapProgressCallback(progressCallback?: (log: string) => void) { + return progressCallback + ? (chunk: Buffer) => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; +} + +export async function runStackCommand( + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void + ) => Promise, + action: string +): Promise { + try { + logger.debug( + `Starting runStackCommand for stack_id=${stack_id}, action="${action}"` + ); + + const stackName = await getStackName(stack_id); + logger.debug( + `Retrieved stack name "${stackName}" for stack_id=${stack_id}` + ); + + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); + + const progressCallback = (log: string) => { + const message = log.trim(); + logger.debug( + `Progress for stack_id=${stack_id}, action="${action}": ${message}` + ); + + if (message.includes("Error response from daemon")) { + const extracted = message.match(/Error response from daemon: (.+)/); + if (extracted) { + logger.error(`Error response from daemon: ${extracted[1]}`); + } + } + + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message, + timestamp: new Date().toISOString(), + }, + }); + }; + + logger.debug( + `Executing command for stack_id=${stack_id}, action="${action}"` + ); + const result = await command(stackPath, progressCallback); + logger.debug( + `Successfully completed command for stack_id=${stack_id}, action="${action}"` + ); + + return result; + } catch (error: unknown) { + const errorMsg = + error instanceof Error ? error.message : JSON.stringify(error); + logger.debug( + `Error occurred for stack_id=${stack_id}, action="${action}": ${errorMsg}` + ); + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(`Error while ${action} stack "${stack_id}": ${errorMsg}`); + } +} diff --git a/src/core/stacks/operations/stackHelpers.ts b/src/core/stacks/operations/stackHelpers.ts new file mode 100644 index 00000000..1a46dd58 --- /dev/null +++ b/src/core/stacks/operations/stackHelpers.ts @@ -0,0 +1,35 @@ +import { dbFunctions } from "~/core/database"; +import { logger } from "~/core/utils/logger"; +import type { Stack } from "~/typings/docker-compose"; +import { findObjectByKey } from "~/core/utils/helpers"; +import YAML from "yaml"; + +export async function getStackName(stack_id: number): Promise { + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = findObjectByKey(stacks, "id", stack_id); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; +} + +export async function getStackPath(stack: Stack): Promise { + const stackName = stack.name.trim().replace(/\s+/g, "_"); + const stackId = stack.id; + + if (!stackId) { + logger.error("Stack could not be parsed"); + throw new Error("Stack could not be parsed"); + } + + return `stacks/${stackId}-${stackName}`; +} + +export async function createStackYAML(compose_spec: Stack): Promise { + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); +} diff --git a/src/core/stacks/operations/stackStatus.ts b/src/core/stacks/operations/stackStatus.ts new file mode 100644 index 00000000..fd399121 --- /dev/null +++ b/src/core/stacks/operations/stackStatus.ts @@ -0,0 +1,89 @@ +import { runStackCommand } from "./runStackCommand"; +import { dbFunctions } from "~/core/database"; +import { logger } from "~/core/utils/logger"; +import DockerCompose from "docker-compose"; + +interface DockerServiceStatus { + status: string; + ports: string[]; +} + +interface StackStatus { + services: Record; + healthy: number; + unhealthy: number; + total: number; +} + +type StacksStatus = Record; + +export async function getStackStatus( + stack_id: number + //biome-ignore lint/suspicious/noExplicitAny: +): Promise> { + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check" + ); + return status; +} + +export async function getAllStacksStatus(): Promise { + try { + const stacks = dbFunctions.getStacks(); + + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.id as number, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + const services = rawStatus.data.services.reduce( + (acc: Record, service) => { + acc[service.name] = { + status: service.state, + ports: service.ports.map( + (port) => `${port.mapped?.address}:${port.mapped?.port}` + ), + }; + return acc; + }, + {} + ); + + const statusValues = Object.values(services); + return { + services, + healthy: statusValues.filter( + (s) => s.status === "running" || s.status.includes("Up") + ).length, + unhealthy: statusValues.filter( + (s) => s.status !== "running" && !s.status.includes("Up") + ).length, + total: statusValues.length, + }; + }, + "status-check" + ); + return { stackId: stack.id, status }; + }) + ); + + return statusResults.reduce((acc, { stackId, status }) => { + acc[String(stackId)] = status; + return acc; + }, {} as StacksStatus); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } +} diff --git a/src/index.ts b/src/index.ts index c8189133..3ddca07f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import cors from "@elysiajs/cors"; import { serverTiming } from "@elysiajs/server-timing"; import staticPlugin from "@elysiajs/static"; import { swagger } from "@elysiajs/swagger"; @@ -10,9 +11,9 @@ import { setSchedules } from "~/core/docker/scheduler"; import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { - authorWebsite, - contributors, - license, + authorWebsite, + contributors, + license, } from "~/core/utils/package-json"; import { swaggerReadme } from "~/core/utils/swagger-readme"; import { validateApiKey } from "~/middleware/auth"; @@ -32,151 +33,152 @@ console.log(""); logger.info("Starting DockStatAPI"); const DockStatAPI = new Elysia({ - normalize: true, - precompile: true, + normalize: true, + precompile: true, }) - .use(Logestic.preset("fancy")) - .use(staticPlugin()) - .use(serverTiming()) - .use( - dts("./src/index.ts", { - tsconfig: "./tsconfig.json", - compilerOptions: { - strict: true, - }, - }), - ) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey" as const, - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }), - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; - - if ( - path === "/health" || - path.startsWith("/swagger") || - path.startsWith("/public") - ) { - logger.info(`Requested unguarded route: ${path}`); - return; - } - - const validation = await validateApiKey(request, set); - - if (!validation) { - throw new Error("Error while checking API key"); - } - - if (!validation.success) { - set.status = 400; - - throw new Error(validation.error); - } - }) - .onError(({ code, set, path, error }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - - logger.error(`Internal server error at ${path}: ${error.message}`); - set.status = 500; - set.headers["Content-Type"] = "text/html"; - return { success: false, message: error.message }; - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(stackRoutes) - .use(liveLogs) - .use(liveStacks) - .get("/health", () => ({ status: "healthy" }), { - tags: ["Utils"], - response: { message: "healthy" }, - }) - .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }); + .use(cors()) + //.use(Logestic.preset("fancy")) + .use(staticPlugin()) + .use(serverTiming()) + .use( + dts("./src/index.ts", { + tsconfig: "./tsconfig.json", + compilerOptions: { + strict: true, + }, + }) + ) + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "3.0.0", + description: swaggerReadme, + }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey" as const, + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], + }, + }) + ) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; + + if ( + path === "/health" || + path.startsWith("/swagger") || + path.startsWith("/public") + ) { + logger.info(`Requested unguarded route: ${path}`); + return; + } + + const validation = await validateApiKey(request, set); + + if (!validation) { + throw new Error("Error while checking API key"); + } + + if (!validation.success) { + set.status = 400; + + throw new Error(validation.error); + } + }) + .onError(({ code, set, path, error }) => { + if (code === "NOT_FOUND") { + logger.warn(`Unknown route (${path}), showing error page!`); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); + } + + logger.error(`Internal server error at ${path}: ${error.message}`); + set.status = 500; + set.headers["Content-Type"] = "text/html"; + return { success: false, message: error.message }; + }) + .use(dockerRoutes) + .use(dockerStatsRoutes) + .use(backendLogs) + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .use(stackRoutes) + .use(liveLogs) + .use(liveStacks) + .get("/health", () => ({ status: "healthy" }), { + tags: ["Utils"], + response: { message: "healthy" }, + }) + .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger` + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + }); const initializeServer = async () => { - try { - await loadPlugins("./src/plugins"); - await setSchedules(); - - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); - - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; - - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", - ); - } - - await checkStacks(); - - logger.info("Started server"); - console.log("----- [ ############## ]"); - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } + try { + await loadPlugins("./src/plugins"); + await setSchedules(); + + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); + + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; + + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" + ); + } + + await checkStacks(); + + logger.info("Started server"); + console.log("----- [ ############## ]"); + } catch (error) { + logger.error("Error while starting server:", error); + process.exit(1); + } }; await initializeServer(); diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 3513901d..8cdec80f 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -381,12 +381,13 @@ export const apiConfigRoutes = new Elysia({ prefix: "/config" }) ) .get( "/backup", - async ({ set }) => { + async () => { try { const backupFiles = readdirSync(backupDir); const filteredFiles = backupFiles.filter((file: string) => { return !( + file.startsWith(".") || file.endsWith(".db") || file.endsWith(".db-shm") || file.endsWith(".db-wal") diff --git a/tsconfig.json b/tsconfig.json index dad4550b..85c0ed8c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,107 +1,109 @@ { - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + "jsx": "react", + "jsxFactory": "Html.createElement", + "jsxFragmentFactory": "Html.Fragment", + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + "composite": true /* Enable constraints that allow a TypeScript project to be used with project references. */, + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + "outDir": "build/", + /* Modules */ + "module": "ES2022" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + "paths": { + "~/*": ["./src/*"] + } /* Specify a set of entries that re-map imports to additional lookup locations. */, + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": [ + "bun-types" + ] /* Specify type package names to be included without being referenced in a source file. */, + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "resolveJsonModule": true /* Enable importing .json files. */, + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* Language and Environment */ - "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - "outDir": "build/", - /* Modules */ - "module": "ES2022" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - "paths": { - "~/*": ["./src/*"] - } /* Specify a set of entries that re-map imports to additional lookup locations. */, - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": [ - "bun-types" - ] /* Specify type package names to be included without being referenced in a source file. */, - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - "resolveJsonModule": true /* Enable importing .json files. */, - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + "inlineSourceMap": true /* Include sourcemap files inside the emitted JavaScript. */, + "inlineSources": true /* Include source code in the sourcemaps inside the emitted JavaScript. */, + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } } From 1177e7e5d90d0f675719b60c669f05f2871024ca Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 7 Jun 2025 09:18:04 +0200 Subject: [PATCH 329/369] Feat: Minor refactor and bug fixes --- package.json | 2 +- src/core/stacks/checker.ts | 11 +- src/core/stacks/controller.ts | 414 ++++++++++-------- src/core/stacks/operations/runStackCommand.ts | 146 +++--- src/core/stacks/operations/stackHelpers.ts | 42 +- src/core/stacks/operations/stackStatus.ts | 138 +++--- src/index.ts | 292 ++++++------ 7 files changed, 539 insertions(+), 506 deletions(-) diff --git a/package.json b/package.json index a3828ff0..118ba2b5 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "build:docker": "docker build -f docker/Dockerfile . -t 'dockstatapi:local'", "clean": "bun run clean:win || bun run clean:lin", "clean:win": "node -e \"process.exit(process.platform === 'win32' ? 0 : 1)\" && cmd /c del /Q data/dockstatapi* && cmd /c del /Q stacks/* && cmd /c del /Q reports/markdown/*.md && echo 'success'", - "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi* && rm -rf stacks/* && rm -f reports/markdown/*.md && echo 'success'", + "clean:lin": "node -e \"process.exit(process.platform !== 'win32' ? 0 : 1)\" && rm -f data/dockstatapi* && sudo rm -rf stacks/* && rm -f reports/markdown/*.md && echo 'success'", "knip": "knip", "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src" }, diff --git a/src/core/stacks/checker.ts b/src/core/stacks/checker.ts index 8d6052e7..9913cafd 100644 --- a/src/core/stacks/checker.ts +++ b/src/core/stacks/checker.ts @@ -8,10 +8,13 @@ export async function checkStacks() { logger.debug(`Checking ${stacks.length} stack(s)`); for (const stack of stacks) { try { - logger.debug(`Checking ${stack.id}`); - const composeFile = Bun.file( - `stacks/${stack.id}-${stack.name}/docker-compose.yaml`, - ); + const composeFilePath = + `stacks/${stack.id}-${stack.name}/docker-compose.yaml`.replaceAll( + " ", + "_", + ); + const composeFile = Bun.file(composeFilePath); + logger.debug(`Checking ${stack.id} - ${composeFilePath}`); if (!(await composeFile.exists())) { logger.error(`Stack (${stack.id} - ${stack.name}) has no compose file`); diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 0899dd1f..90f7c671 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -1,218 +1,248 @@ -import { runStackCommand } from "./operations/runStackCommand"; -import { - getStackPath, - createStackYAML, - getStackName, -} from "./operations/stackHelpers"; +import { rm } from "node:fs/promises"; +import DockerCompose from "docker-compose"; import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; import { postToClient } from "~/routes/live-stacks"; import type { stacks_config } from "~/typings/database"; -import { checkStacks } from "./checker"; import type { Stack } from "~/typings/docker-compose"; -import { wrapProgressCallback } from "./operations/runStackCommand"; import type { ComposeSpec } from "~/typings/docker-compose"; -import { rm } from "node:fs/promises"; -import DockerCompose from "docker-compose"; +import { checkStacks } from "./checker"; +import { runStackCommand } from "./operations/runStackCommand"; +import { wrapProgressCallback } from "./operations/runStackCommand"; +import { + createStackYAML, + getStackName, + getStackPath, +} from "./operations/stackHelpers"; export async function deployStack(stack_config: stacks_config): Promise { - let stackId: number | null = null; - let stackPath = ""; - - try { - logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); - if (!stack_config.name) throw new Error("Stack name needed"); - - const jsonStringStack = { - ...stack_config, - compose_spec: JSON.stringify(stack_config.compose_spec), - }; - - stackId = dbFunctions.addStack(jsonStringStack) || null; - if (!stackId) { - throw new Error("Failed to add stack to database"); - } - - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "pending", - message: "Creating stack configuration", - }, - }); - - const stackYaml: Stack = { - id: stackId, - name: stack_config.name, - source: stack_config.source, - version: stack_config.version, - compose_spec: stack_config.compose_spec as unknown as ComposeSpec, - }; - - await createStackYAML(stackYaml); - stackPath = await getStackPath(stackYaml); - - await runStackCommand( - stackId, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "deploying" - ); - - postToClient({ - type: "stack-status", - data: { - stack_id: stackId, - status: "deployed", - message: "Stack deployed successfully", - }, - }); - - await checkStacks(); - } catch (error: unknown) { - const errorMsg = - error instanceof Error ? error.message : JSON.stringify(error); - logger.error(errorMsg); - if (stackId !== null) { - dbFunctions.deleteStack(stackId); - if (stackPath) { - try { - await rm(stackPath, { recursive: true }); - } catch (cleanupError) { - logger.error(`Error cleaning up stack path: ${cleanupError}`); - } - } - } - postToClient({ - type: "stack-error", - data: { - stack_id: stackId ?? 0, - action: "deploying", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + let stackId: number | null = null; + let stackPath = ""; + + try { + logger.debug(`Deploying Stack: ${JSON.stringify(stack_config)}`); + if (!stack_config.name) throw new Error("Stack name needed"); + + const jsonStringStack = { + ...stack_config, + compose_spec: JSON.stringify(stack_config.compose_spec), + }; + + stackId = dbFunctions.addStack(jsonStringStack) || null; + if (!stackId) { + throw new Error("Failed to add stack to database"); + } + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, + }); + + const stackYaml: Stack = { + id: stackId, + name: stack_config.name, + source: stack_config.source, + version: stack_config.version, + compose_spec: stack_config.compose_spec as unknown as ComposeSpec, + }; + + await createStackYAML(stackYaml); + stackPath = await getStackPath(stackYaml); + + await runStackCommand( + stackId, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "deploying", + ); + + postToClient({ + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, + }); + + await checkStacks(); + } catch (error: unknown) { + const errorMsg = + error instanceof Error ? error.message : JSON.stringify(error); + logger.error(errorMsg); + + if (stackId !== null) { + // Attempt to remove any containers created during failed deployment + if (stackPath) { + try { + await DockerCompose.down({ + cwd: stackPath, + log: false, // No need for progress logging during cleanup + }); + } catch (downError) { + const downErrorMsg = + downError instanceof Error + ? downError.message + : JSON.stringify(downError); + logger.error(`Failed to cleanup containers: ${downErrorMsg}`); + } + } + + // Proceed with existing cleanup (DB and filesystem) + dbFunctions.deleteStack(stackId); + if (stackPath) { + try { + await rm(stackPath, { recursive: true }); + } catch (cleanupError) { + logger.error(`Error cleaning up stack path: ${cleanupError}`); + } + } + } + + postToClient({ + type: "stack-error", + data: { + stack_id: stackId ?? 0, + action: "deploying", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } export async function stopStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.downAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "stopping" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.down({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "stopping", + ); } export async function startStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.upAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "starting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.upAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "starting", + ); } export async function pullStackImages(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.pullAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "pulling-images" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.pullAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "pulling-images", + ); } export async function restartStack(stack_id: number): Promise { - await runStackCommand( - stack_id, - (cwd, progressCallback) => - DockerCompose.restartAll({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }), - "restarting" - ); + await runStackCommand( + stack_id, + (cwd, progressCallback) => + DockerCompose.restartAll({ + cwd, + log: true, + callback: wrapProgressCallback(progressCallback), + }), + "restarting", + ); } export async function removeStack(stack_id: number): Promise { - try { - const _ = dbFunctions.deleteStack(stack_id); - - await runStackCommand( - stack_id, - async (cwd, progressCallback) => { - await DockerCompose.down({ - cwd, - log: true, - callback: wrapProgressCallback(progressCallback), - }); - }, - "removing" - ); - - const stackName = await getStackName(stack_id); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - - try { - await rm(stackPath, { recursive: true }); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } - - postToClient({ - type: "stack-removed", - data: { - stack_id, - message: "Stack removed successfully", - }, - }); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - postToClient({ - type: "stack-error", - data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(errorMsg); - } + try { + if (!stack_id) { + throw new Error("Stack ID needed"); + } + + await runStackCommand( + stack_id, + async (cwd, progressCallback) => { + await DockerCompose.down({ + cwd, + // Add 'volumes' flag to remove named volumes + commandOptions: ["--volumes", "--remove-orphans"], + log: true, + callback: wrapProgressCallback(progressCallback), + }); + }, + "removing", + ); + + const stackName = await getStackName(stack_id); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + + try { + await rm(stackPath, { + recursive: true, + force: true, + maxRetries: 3, + retryDelay: 300, + }); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: `Directory removal failed: ${errorMsg}`, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } + + dbFunctions.deleteStack(stack_id); + + postToClient({ + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, + }); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + postToClient({ + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(errorMsg); + } } export { getStackStatus, getAllStacksStatus } from "./operations/stackStatus"; diff --git a/src/core/stacks/operations/runStackCommand.ts b/src/core/stacks/operations/runStackCommand.ts index 14191c60..818e6499 100644 --- a/src/core/stacks/operations/runStackCommand.ts +++ b/src/core/stacks/operations/runStackCommand.ts @@ -1,89 +1,89 @@ -import { getStackName, getStackPath } from "./stackHelpers"; -import { postToClient } from "~/routes/live-stacks"; import { logger } from "~/core/utils/logger"; +import { postToClient } from "~/routes/live-stacks"; import type { Stack } from "~/typings/docker-compose"; +import { getStackName, getStackPath } from "./stackHelpers"; export function wrapProgressCallback(progressCallback?: (log: string) => void) { - return progressCallback - ? (chunk: Buffer) => { - const log = chunk.toString(); - progressCallback(log); - } - : undefined; + return progressCallback + ? (chunk: Buffer) => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; } export async function runStackCommand( - stack_id: number, - command: ( - cwd: string, - progressCallback?: (log: string) => void - ) => Promise, - action: string + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void, + ) => Promise, + action: string, ): Promise { - try { - logger.debug( - `Starting runStackCommand for stack_id=${stack_id}, action="${action}"` - ); + try { + logger.debug( + `Starting runStackCommand for stack_id=${stack_id}, action="${action}"`, + ); - const stackName = await getStackName(stack_id); - logger.debug( - `Retrieved stack name "${stackName}" for stack_id=${stack_id}` - ); + const stackName = await getStackName(stack_id); + logger.debug( + `Retrieved stack name "${stackName}" for stack_id=${stack_id}`, + ); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); - const progressCallback = (log: string) => { - const message = log.trim(); - logger.debug( - `Progress for stack_id=${stack_id}, action="${action}": ${message}` - ); + const progressCallback = (log: string) => { + const message = log.trim(); + logger.debug( + `Progress for stack_id=${stack_id}, action="${action}": ${message}`, + ); - if (message.includes("Error response from daemon")) { - const extracted = message.match(/Error response from daemon: (.+)/); - if (extracted) { - logger.error(`Error response from daemon: ${extracted[1]}`); - } - } + if (message.includes("Error response from daemon")) { + const extracted = message.match(/Error response from daemon: (.+)/); + if (extracted) { + logger.error(`Error response from daemon: ${extracted[1]}`); + } + } - postToClient({ - type: "stack-progress", - data: { - stack_id, - action, - message, - timestamp: new Date().toISOString(), - }, - }); - }; + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message, + timestamp: new Date().toISOString(), + }, + }); + }; - logger.debug( - `Executing command for stack_id=${stack_id}, action="${action}"` - ); - const result = await command(stackPath, progressCallback); - logger.debug( - `Successfully completed command for stack_id=${stack_id}, action="${action}"` - ); + logger.debug( + `Executing command for stack_id=${stack_id}, action="${action}"`, + ); + const result = await command(stackPath, progressCallback); + logger.debug( + `Successfully completed command for stack_id=${stack_id}, action="${action}"`, + ); - return result; - } catch (error: unknown) { - const errorMsg = - error instanceof Error ? error.message : JSON.stringify(error); - logger.debug( - `Error occurred for stack_id=${stack_id}, action="${action}": ${errorMsg}` - ); - postToClient({ - type: "stack-error", - data: { - stack_id, - action, - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(`Error while ${action} stack "${stack_id}": ${errorMsg}`); - } + return result; + } catch (error: unknown) { + const errorMsg = + error instanceof Error ? error.message : JSON.stringify(error); + logger.debug( + `Error occurred for stack_id=${stack_id}, action="${action}": ${errorMsg}`, + ); + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(`Error while ${action} stack "${stack_id}": ${errorMsg}`); + } } diff --git a/src/core/stacks/operations/stackHelpers.ts b/src/core/stacks/operations/stackHelpers.ts index 1a46dd58..ab01b69d 100644 --- a/src/core/stacks/operations/stackHelpers.ts +++ b/src/core/stacks/operations/stackHelpers.ts @@ -1,35 +1,35 @@ +import YAML from "yaml"; import { dbFunctions } from "~/core/database"; +import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; import type { Stack } from "~/typings/docker-compose"; -import { findObjectByKey } from "~/core/utils/helpers"; -import YAML from "yaml"; export async function getStackName(stack_id: number): Promise { - logger.debug(`Fetching stack name for id ${stack_id}`); - const stacks = dbFunctions.getStacks(); - const stack = findObjectByKey(stacks, "id", stack_id); - if (!stack) { - throw new Error(`Stack with id ${stack_id} not found`); - } - return stack.name; + logger.debug(`Fetching stack name for id ${stack_id}`); + const stacks = dbFunctions.getStacks(); + const stack = findObjectByKey(stacks, "id", stack_id); + if (!stack) { + throw new Error(`Stack with id ${stack_id} not found`); + } + return stack.name; } export async function getStackPath(stack: Stack): Promise { - const stackName = stack.name.trim().replace(/\s+/g, "_"); - const stackId = stack.id; + const stackName = stack.name.trim().replace(/\s+/g, "_"); + const stackId = stack.id; - if (!stackId) { - logger.error("Stack could not be parsed"); - throw new Error("Stack could not be parsed"); - } + if (!stackId) { + logger.error("Stack could not be parsed"); + throw new Error("Stack could not be parsed"); + } - return `stacks/${stackId}-${stackName}`; + return `stacks/${stackId}-${stackName}`; } export async function createStackYAML(compose_spec: Stack): Promise { - const yaml = YAML.stringify(compose_spec.compose_spec); - const stackPath = await getStackPath(compose_spec); - await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { - createPath: true, - }); + const yaml = YAML.stringify(compose_spec.compose_spec); + const stackPath = await getStackPath(compose_spec); + await Bun.write(`${stackPath}/docker-compose.yaml`, yaml, { + createPath: true, + }); } diff --git a/src/core/stacks/operations/stackStatus.ts b/src/core/stacks/operations/stackStatus.ts index fd399121..b29e6e34 100644 --- a/src/core/stacks/operations/stackStatus.ts +++ b/src/core/stacks/operations/stackStatus.ts @@ -1,89 +1,89 @@ -import { runStackCommand } from "./runStackCommand"; +import DockerCompose from "docker-compose"; import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; -import DockerCompose from "docker-compose"; +import { runStackCommand } from "./runStackCommand"; interface DockerServiceStatus { - status: string; - ports: string[]; + status: string; + ports: string[]; } interface StackStatus { - services: Record; - healthy: number; - unhealthy: number; - total: number; + services: Record; + healthy: number; + unhealthy: number; + total: number; } type StacksStatus = Record; export async function getStackStatus( - stack_id: number - //biome-ignore lint/suspicious/noExplicitAny: + stack_id: number, + //biome-ignore lint/suspicious/noExplicitAny: ): Promise> { - const status = await runStackCommand( - stack_id, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - //biome-ignore lint/suspicious/noExplicitAny: - return rawStatus.data.services.reduce((acc: any, service: any) => { - acc[service.name] = service.state; - return acc; - }, {}); - }, - "status-check" - ); - return status; + const status = await runStackCommand( + stack_id, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + //biome-ignore lint/suspicious/noExplicitAny: + return rawStatus.data.services.reduce((acc: any, service: any) => { + acc[service.name] = service.state; + return acc; + }, {}); + }, + "status-check", + ); + return status; } export async function getAllStacksStatus(): Promise { - try { - const stacks = dbFunctions.getStacks(); + try { + const stacks = dbFunctions.getStacks(); - const statusResults = await Promise.all( - stacks.map(async (stack) => { - const status = await runStackCommand( - stack.id as number, - async (cwd) => { - const rawStatus = await DockerCompose.ps({ cwd }); - const services = rawStatus.data.services.reduce( - (acc: Record, service) => { - acc[service.name] = { - status: service.state, - ports: service.ports.map( - (port) => `${port.mapped?.address}:${port.mapped?.port}` - ), - }; - return acc; - }, - {} - ); + const statusResults = await Promise.all( + stacks.map(async (stack) => { + const status = await runStackCommand( + stack.id as number, + async (cwd) => { + const rawStatus = await DockerCompose.ps({ cwd }); + const services = rawStatus.data.services.reduce( + (acc: Record, service) => { + acc[service.name] = { + status: service.state, + ports: service.ports.map( + (port) => `${port.mapped?.address}:${port.mapped?.port}`, + ), + }; + return acc; + }, + {}, + ); - const statusValues = Object.values(services); - return { - services, - healthy: statusValues.filter( - (s) => s.status === "running" || s.status.includes("Up") - ).length, - unhealthy: statusValues.filter( - (s) => s.status !== "running" && !s.status.includes("Up") - ).length, - total: statusValues.length, - }; - }, - "status-check" - ); - return { stackId: stack.id, status }; - }) - ); + const statusValues = Object.values(services); + return { + services, + healthy: statusValues.filter( + (s) => s.status === "running" || s.status.includes("Up"), + ).length, + unhealthy: statusValues.filter( + (s) => s.status !== "running" && !s.status.includes("Up"), + ).length, + total: statusValues.length, + }; + }, + "status-check", + ); + return { stackId: stack.id, status }; + }), + ); - return statusResults.reduce((acc, { stackId, status }) => { - acc[String(stackId)] = status; - return acc; - }, {} as StacksStatus); - } catch (error: unknown) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(errorMsg); - throw new Error(errorMsg); - } + return statusResults.reduce((acc, { stackId, status }) => { + acc[String(stackId)] = status; + return acc; + }, {} as StacksStatus); + } catch (error: unknown) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(errorMsg); + throw new Error(errorMsg); + } } diff --git a/src/index.ts b/src/index.ts index 3ddca07f..5741cbdd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,9 +11,9 @@ import { setSchedules } from "~/core/docker/scheduler"; import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { - authorWebsite, - contributors, - license, + authorWebsite, + contributors, + license, } from "~/core/utils/package-json"; import { swaggerReadme } from "~/core/utils/swagger-readme"; import { validateApiKey } from "~/middleware/auth"; @@ -33,152 +33,152 @@ console.log(""); logger.info("Starting DockStatAPI"); const DockStatAPI = new Elysia({ - normalize: true, - precompile: true, + normalize: true, + precompile: true, }) - .use(cors()) - //.use(Logestic.preset("fancy")) - .use(staticPlugin()) - .use(serverTiming()) - .use( - dts("./src/index.ts", { - tsconfig: "./tsconfig.json", - compilerOptions: { - strict: true, - }, - }) - ) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey" as const, - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }) - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; - - if ( - path === "/health" || - path.startsWith("/swagger") || - path.startsWith("/public") - ) { - logger.info(`Requested unguarded route: ${path}`); - return; - } - - const validation = await validateApiKey(request, set); - - if (!validation) { - throw new Error("Error while checking API key"); - } - - if (!validation.success) { - set.status = 400; - - throw new Error(validation.error); - } - }) - .onError(({ code, set, path, error }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - - logger.error(`Internal server error at ${path}: ${error.message}`); - set.status = 500; - set.headers["Content-Type"] = "text/html"; - return { success: false, message: error.message }; - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(stackRoutes) - .use(liveLogs) - .use(liveStacks) - .get("/health", () => ({ status: "healthy" }), { - tags: ["Utils"], - response: { message: "healthy" }, - }) - .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger` - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }); + .use(cors()) + //.use(Logestic.preset("fancy")) + .use(staticPlugin()) + .use(serverTiming()) + .use( + dts("./src/index.ts", { + tsconfig: "./tsconfig.json", + compilerOptions: { + strict: true, + }, + }), + ) + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "3.0.0", + description: swaggerReadme, + }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey" as const, + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], + }, + }), + ) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; + + if ( + path === "/health" || + path.startsWith("/swagger") || + path.startsWith("/public") + ) { + logger.info(`Requested unguarded route: ${path}`); + return; + } + + const validation = await validateApiKey(request, set); + + if (!validation) { + throw new Error("Error while checking API key"); + } + + if (!validation.success) { + set.status = 400; + + throw new Error(validation.error); + } + }) + .onError(({ code, set, path, error }) => { + if (code === "NOT_FOUND") { + logger.warn(`Unknown route (${path}), showing error page!`); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); + } + + logger.error(`Internal server error at ${path}: ${error.message}`); + set.status = 500; + set.headers["Content-Type"] = "text/html"; + return { success: false, message: error.message }; + }) + .use(dockerRoutes) + .use(dockerStatsRoutes) + .use(backendLogs) + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .use(stackRoutes) + .use(liveLogs) + .use(liveStacks) + .get("/health", () => ({ status: "healthy" }), { + tags: ["Utils"], + response: { message: "healthy" }, + }) + .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + }); const initializeServer = async () => { - try { - await loadPlugins("./src/plugins"); - await setSchedules(); - - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); - - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; - - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" - ); - } - - await checkStacks(); - - logger.info("Started server"); - console.log("----- [ ############## ]"); - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } + try { + await loadPlugins("./src/plugins"); + await setSchedules(); + + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); + + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; + + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", + ); + } + + await checkStacks(); + + logger.info("Started server"); + console.log("----- [ ############## ]"); + } catch (error) { + logger.error("Error while starting server:", error); + process.exit(1); + } }; await initializeServer(); From ffab878a9d536af7aa14dd7b785643f50ac973ec Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 7 Jun 2025 07:19:55 +0000 Subject: [PATCH 330/369] Update dependency graphs --- dependency-graph.mmd | 400 ++++++------ dependency-graph.svg | 1441 +++++++++++++++++++++++------------------- 2 files changed, 1000 insertions(+), 841 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index affe0471..db1c046d 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -8,225 +8,247 @@ flowchart LR subgraph 0["src"] 1["index.ts"] -subgraph 6["routes"] -7["live-stacks.ts"] -X["live-logs.ts"] -1I["api-config.ts"] -1K["docker-manager.ts"] -1L["docker-stats.ts"] -1M["docker-websocket.ts"] -1O["logs.ts"] -1P["stacks.ts"] +subgraph 6["core"] +subgraph 7["stacks"] +8["checker.ts"] +1R["controller.ts"] +subgraph 1T["operations"] +1U["runStackCommand.ts"] +1V["stackHelpers.ts"] +1W["stackStatus.ts"] end -subgraph 9["core"] -subgraph A["utils"] -B["logger.ts"] -W["helpers.ts"] -17["calculations.ts"] -1B["change-me-checker.ts"] -1C["package-json.ts"] -1E["swagger-readme.ts"] -1J["response-handler.ts"] end -subgraph D["database"] +subgraph 9["database"] +A["index.ts"] +B["backup.ts"] E["_dbState.ts"] -F["index.ts"] -G["backup.ts"] -J["database.ts"] -N["helper.ts"] -O["config.ts"] -P["containerStats.ts"] -Q["dockerHosts.ts"] -R["hostStats.ts"] -T["logs.ts"] -U["stacks.ts"] +F["database.ts"] +K["helper.ts"] +P["config.ts"] +Q["containerStats.ts"] +R["dockerHosts.ts"] +S["hostStats.ts"] +U["logs.ts"] +V["stacks.ts"] end -subgraph Y["docker"] -Z["monitor.ts"] -14["client.ts"] -15["scheduler.ts"] -16["store-container-stats.ts"] -18["store-host-stats.ts"] +subgraph L["utils"] +M["logger.ts"] +W["helpers.ts"] +18["calculations.ts"] +1C["change-me-checker.ts"] +1D["package-json.ts"] +1F["swagger-readme.ts"] +1K["response-handler.ts"] end -subgraph 10["plugins"] -11["plugin-manager.ts"] -1A["loader.ts"] +subgraph Z["docker"] +10["monitor.ts"] +15["client.ts"] +16["scheduler.ts"] +17["store-container-stats.ts"] +19["store-host-stats.ts"] end -subgraph 1Q["stacks"] -1R["controller.ts"] +subgraph 11["plugins"] +12["plugin-manager.ts"] +1B["loader.ts"] end end -subgraph 1F["middleware"] -1G["auth.ts"] +subgraph N["routes"] +O["live-logs.ts"] +X["live-stacks.ts"] +1J["api-config.ts"] +1L["docker-manager.ts"] +1M["docker-stats.ts"] +1N["docker-websocket.ts"] +1P["logs.ts"] +1Q["stacks.ts"] +end +subgraph 1G["middleware"] +1H["auth.ts"] end end subgraph 2["~"] subgraph 3["typings"] 4["database"] -8["websocket"] -H["misc"] -S["docker"] -V["docker-compose"] -12["plugin"] -19["dockerode"] -1H["elysiajs"] +C["misc"] +T["docker"] +Y["websocket"] +13["plugin"] +1A["dockerode"] +1I["elysiajs"] +1S["docker-compose"] end end 5["elysia-remote-dts"] -C["path"] -subgraph I["fs"] -L["promises"] +subgraph D["fs"] +H["promises"] end -K["bun:sqlite"] -M["os"] -13["events"] -1D["package.json"] -1N["stream"] -1-->7 -1-->F -1-->Z -1-->15 -1-->1A -1-->B -1-->1C -1-->1E -1-->1G -1-->1I -1-->1K +G["bun:sqlite"] +I["os"] +J["path"] +14["events"] +1E["package.json"] +1O["stream"] +1-->8 +1-->X +1-->A +1-->10 +1-->16 +1-->1B +1-->M +1-->1D +1-->1F +1-->1H +1-->1J 1-->1L 1-->1M -1-->X -1-->1O +1-->1N +1-->O 1-->1P +1-->1Q 1-->4 1-->5 -7-->B -7-->8 +8-->A +8-->M +A-->B +A-->P +A-->Q +A-->F +A-->R +A-->S +A-->U +A-->V B-->E B-->F -B-->X -B-->4 +B-->K +B-->M B-->C +B-->D F-->G -F-->O -F-->P +F-->D +F-->H +F-->I F-->J -F-->Q -F-->R -F-->T -F-->U -G-->E -G-->J -G-->N -G-->B -G-->H -G-->I -J-->K -J-->I -J-->L -J-->M -J-->C -N-->E -N-->B -O-->J -O-->N -P-->J -P-->N -Q-->J -Q-->N -R-->J -R-->N -R-->S -T-->J -T-->N -T-->4 -U-->W -U-->J -U-->N +K-->E +K-->M +M-->E +M-->A +M-->O +M-->4 +M-->J +O-->M +O-->4 +P-->F +P-->K +Q-->F +Q-->K +R-->F +R-->K +S-->F +S-->K +S-->T +U-->F +U-->K U-->4 -U-->V -W-->B -X-->B -X-->4 -Z-->11 -Z-->F -Z-->14 -Z-->B -Z-->S -11-->B -11-->S -11-->12 -11-->13 -14-->B -14-->S -15-->F -15-->16 -15-->18 -15-->B -15-->4 -16-->B -16-->F -16-->14 +V-->W +V-->F +V-->K +V-->4 +W-->M +X-->M +X-->Y +10-->12 +10-->A +10-->15 +10-->M +10-->T +12-->M +12-->T +12-->13 +12-->14 +15-->M +15-->T +16-->A 16-->17 -18-->F -18-->14 -18-->W -18-->B -18-->S -18-->19 -1A-->1B -1A-->B -1A-->11 -1A-->I -1A-->C -1B-->B -1B-->L -1C-->1D -1G-->F -1G-->B -1G-->4 -1G-->1H -1I-->F -1I-->G -1I-->11 -1I-->B -1I-->1C -1I-->1J -1I-->1G -1I-->4 -1I-->I +16-->19 +16-->M +16-->4 +17-->M +17-->A +17-->15 +17-->18 +19-->A +19-->15 +19-->W +19-->M +19-->T +19-->1A +1B-->1C +1B-->M +1B-->12 +1B-->D +1B-->J +1C-->M +1C-->H +1D-->1E +1H-->A +1H-->M +1H-->4 +1H-->1I +1J-->A 1J-->B +1J-->12 +1J-->M +1J-->1D +1J-->1K 1J-->1H -1K-->F -1K-->B -1K-->1J -1K-->S -1L-->F -1L-->14 -1L-->17 -1L-->W -1L-->B -1L-->1J -1L-->S -1L-->19 -1M-->F -1M-->14 -1M-->17 -1M-->B -1M-->1J -1M-->1N -1O-->F -1O-->B -1P-->F -1P-->1R -1P-->B -1P-->1J -1P-->4 -1R-->W -1R-->F -1R-->B -1R-->7 +1J-->4 +1J-->D +1K-->M +1K-->1I +1L-->A +1L-->M +1L-->1K +1L-->T +1M-->A +1M-->15 +1M-->18 +1M-->W +1M-->M +1M-->1K +1M-->T +1M-->1A +1N-->A +1N-->15 +1N-->18 +1N-->M +1N-->1K +1N-->1O +1P-->A +1P-->M +1Q-->A +1Q-->1R +1Q-->M +1Q-->1K +1Q-->4 +1R-->8 +1R-->1U +1R-->1V +1R-->1W +1R-->A +1R-->M +1R-->X 1R-->4 -1R-->V -1R-->L +1R-->1S +1R-->H +1U-->1V +1U-->M +1U-->X +1U-->1S +1V-->A +1V-->W +1V-->M +1V-->1S +1W-->1U +1W-->A +1W-->M diff --git a/dependency-graph.svg b/dependency-graph.svg index 8e7e54b0..54234f89 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,77 +4,82 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks -cluster_src/core/utils - -utils +cluster_src/core/stacks/operations + +operations -cluster_src/middleware - -middleware +cluster_src/core/utils + +utils -cluster_src/routes - -routes +cluster_src/middleware + +middleware -cluster_~ - -~ +cluster_src/routes + +routes +cluster_~ + +~ + + cluster_~/typings - -typings + +typings bun:sqlite - -bun:sqlite + +bun:sqlite @@ -82,8 +87,8 @@ elysia-remote-dts - -elysia-remote-dts + +elysia-remote-dts @@ -91,8 +96,8 @@ events - -events + +events @@ -100,8 +105,8 @@ fs - -fs + +fs @@ -109,8 +114,8 @@ fs/promises - -promises + +promises @@ -118,8 +123,8 @@ os - -os + +os @@ -127,8 +132,8 @@ package.json - -package.json + +package.json @@ -136,8 +141,8 @@ path - -path + +path @@ -145,8 +150,8 @@ src/core/database/_dbState.ts - -_dbState.ts + +_dbState.ts @@ -154,1321 +159,1453 @@ src/core/database/backup.ts - -backup.ts + +backup.ts src/core/database/backup.ts->fs - - + + src/core/database/backup.ts->src/core/database/_dbState.ts - - + + src/core/database/database.ts - -database.ts + +database.ts src/core/database/backup.ts->src/core/database/database.ts - - + + src/core/database/helper.ts - -helper.ts + +helper.ts src/core/database/backup.ts->src/core/database/helper.ts - - - - + + + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + ~/typings/misc - -misc + +misc src/core/database/backup.ts->~/typings/misc - - + + src/core/database/database.ts->bun:sqlite - - + + src/core/database/database.ts->fs - - + + src/core/database/database.ts->fs/promises - - + + src/core/database/database.ts->os - - + + src/core/database/database.ts->path - - + + src/core/database/helper.ts->src/core/database/_dbState.ts - - + + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + - + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + src/core/database/index.ts - -index.ts + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + ~/typings/database - -database + +database - + src/core/utils/logger.ts->~/typings/database - - + + - + src/routes/live-logs.ts - - -live-logs.ts + + +live-logs.ts - + src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + + + src/core/database/config.ts - -config.ts + +config.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + ~/typings/docker - -docker + +docker src/core/database/hostStats.ts->~/typings/docker - - + + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + src/core/database/logs.ts->~/typings/database - - + + src/core/database/stacks.ts->src/core/database/database.ts - - + + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + src/core/database/stacks.ts->~/typings/database - - + + src/core/utils/helpers.ts - -helpers.ts + +helpers.ts src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - - - - -~/typings/docker-compose - - -docker-compose - - - - - -src/core/database/stacks.ts->~/typings/docker-compose - - + + + + - + src/core/utils/helpers.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/docker/client.ts - - -client.ts + + +client.ts - + src/core/docker/client.ts->src/core/utils/logger.ts - - + + - + src/core/docker/client.ts->~/typings/docker - - + + - + src/core/docker/monitor.ts - - -monitor.ts + + +monitor.ts - + src/core/docker/monitor.ts->src/core/utils/logger.ts - - + + - + src/core/docker/monitor.ts->~/typings/docker - - + + - + src/core/docker/monitor.ts->src/core/database/index.ts - - + + - + src/core/docker/monitor.ts->src/core/docker/client.ts - - + + - + src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +plugin-manager.ts - + src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->~/typings/docker - - + + - + ~/typings/plugin - - -plugin + + +plugin - + src/core/plugins/plugin-manager.ts->~/typings/plugin - - + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + - + src/core/docker/scheduler.ts->~/typings/database - - + + - + src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - - -calculations.ts + + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-host-stats.ts->~/typings/docker - - + + - + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + ~/typings/dockerode - - -dockerode + + +dockerode - + src/core/docker/store-host-stats.ts->~/typings/dockerode - - + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + + + + +src/core/stacks/checker.ts + + +checker.ts + + + + + +src/core/stacks/checker.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/checker.ts->src/core/database/index.ts + + src/core/stacks/controller.ts - -controller.ts + +controller.ts - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts->~/typings/database - - + + - - -src/core/stacks/controller.ts->src/core/utils/helpers.ts - - + + +src/core/stacks/controller.ts->src/core/stacks/checker.ts + + - - -src/core/stacks/controller.ts->~/typings/docker-compose - - + + +src/core/stacks/operations/runStackCommand.ts + + +runStackCommand.ts + + + + + +src/core/stacks/controller.ts->src/core/stacks/operations/runStackCommand.ts + + + + + +src/core/stacks/operations/stackHelpers.ts + + +stackHelpers.ts + + + + + +src/core/stacks/controller.ts->src/core/stacks/operations/stackHelpers.ts + + + + + +src/core/stacks/operations/stackStatus.ts + + +stackStatus.ts + + + + + +src/core/stacks/controller.ts->src/core/stacks/operations/stackStatus.ts + + - + src/routes/live-stacks.ts - - -live-stacks.ts + + +live-stacks.ts - + src/core/stacks/controller.ts->src/routes/live-stacks.ts - - + + + + + +~/typings/docker-compose + + +docker-compose + + + + + +src/core/stacks/controller.ts->~/typings/docker-compose + + + + + +src/core/stacks/operations/runStackCommand.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/operations/runStackCommand.ts->src/core/stacks/operations/stackHelpers.ts + + + + + +src/core/stacks/operations/runStackCommand.ts->src/routes/live-stacks.ts + + + + + +src/core/stacks/operations/runStackCommand.ts->~/typings/docker-compose + + + + + +src/core/stacks/operations/stackHelpers.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/operations/stackHelpers.ts->src/core/database/index.ts + + + + + +src/core/stacks/operations/stackHelpers.ts->src/core/utils/helpers.ts + + + + + +src/core/stacks/operations/stackHelpers.ts->~/typings/docker-compose + + + + + +src/core/stacks/operations/stackStatus.ts->src/core/utils/logger.ts + + + + + +src/core/stacks/operations/stackStatus.ts->src/core/database/index.ts + + + + + +src/core/stacks/operations/stackStatus.ts->src/core/stacks/operations/runStackCommand.ts + + - + src/routes/live-stacks.ts->src/core/utils/logger.ts - - + + - + ~/typings/websocket - - -websocket + + +websocket - + src/routes/live-stacks.ts->~/typings/websocket - - + + - + src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + + + - + src/routes/live-logs.ts->~/typings/database - - + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - + src/core/utils/response-handler.ts - - -response-handler.ts + + +response-handler.ts - + src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + - + ~/typings/elysiajs - - -elysiajs + + +elysiajs - + src/core/utils/response-handler.ts->~/typings/elysiajs - - + + - + src/core/utils/swagger-readme.ts - - -swagger-readme.ts + + +swagger-readme.ts - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->elysia-remote-dts - - + + - + src/index.ts->src/core/utils/logger.ts - - + + - + src/index.ts->src/core/database/index.ts - - + + - + src/index.ts->~/typings/database - - + + - + src/index.ts->src/core/docker/monitor.ts - - + + - + src/index.ts->src/core/docker/scheduler.ts - - + + - + src/index.ts->src/core/plugins/loader.ts - - + + + + + +src/index.ts->src/core/stacks/checker.ts + + - + src/index.ts->src/routes/live-stacks.ts - - + + - + src/index.ts->src/routes/live-logs.ts - - + + - + src/index.ts->src/core/utils/package-json.ts - - + + - + src/index.ts->src/core/utils/swagger-readme.ts - - + + - + src/middleware/auth.ts - - -auth.ts + + +auth.ts - + src/index.ts->src/middleware/auth.ts - - + + - + src/routes/api-config.ts - - -api-config.ts + + +api-config.ts - + src/index.ts->src/routes/api-config.ts - - + + - + src/routes/docker-manager.ts - - -docker-manager.ts + + +docker-manager.ts - + src/index.ts->src/routes/docker-manager.ts - - + + - + src/routes/docker-stats.ts - - -docker-stats.ts + + +docker-stats.ts - + src/index.ts->src/routes/docker-stats.ts - - + + - + src/routes/docker-websocket.ts - - -docker-websocket.ts + + +docker-websocket.ts - + src/index.ts->src/routes/docker-websocket.ts - - + + - + src/routes/logs.ts - - -logs.ts + + +logs.ts - + src/index.ts->src/routes/logs.ts - - + + - + src/routes/stacks.ts - - -stacks.ts + + +stacks.ts - + src/index.ts->src/routes/stacks.ts - - + + - + src/middleware/auth.ts->src/core/utils/logger.ts - - + + - + src/middleware/auth.ts->src/core/database/index.ts - - + + - + src/middleware/auth.ts->~/typings/database - - + + - + src/middleware/auth.ts->~/typings/elysiajs - - + + - + src/routes/api-config.ts->fs - - + + - + src/routes/api-config.ts->src/core/database/backup.ts - - + + - + src/routes/api-config.ts->src/core/utils/logger.ts - - + + - + src/routes/api-config.ts->src/core/database/index.ts - - + + - + src/routes/api-config.ts->~/typings/database - - + + - + src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/routes/api-config.ts->src/core/utils/package-json.ts - - + + - + src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + - + src/routes/api-config.ts->src/middleware/auth.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-manager.ts->~/typings/docker - - + + - + src/routes/docker-manager.ts->src/core/database/index.ts - - + + - + src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-stats.ts->~/typings/docker - - + + - + src/routes/docker-stats.ts->src/core/database/index.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/helpers.ts - - + + - + src/routes/docker-stats.ts->src/core/docker/client.ts - - + + - + src/routes/docker-stats.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-stats.ts->~/typings/dockerode - - + + - + src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/logger.ts - - + + - + src/routes/docker-websocket.ts->src/core/database/index.ts - - + + - + src/routes/docker-websocket.ts->src/core/docker/client.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - + + - + src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + - + stream - - -stream + + +stream - + src/routes/docker-websocket.ts->stream - - + + - + src/routes/logs.ts->src/core/utils/logger.ts - - + + - + src/routes/logs.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->src/core/utils/logger.ts - - + + - + src/routes/stacks.ts->src/core/database/index.ts - - + + - + src/routes/stacks.ts->~/typings/database - - + + - + src/routes/stacks.ts->src/core/stacks/controller.ts - - + + - + src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + From ef74b6a64d91d0683a9962e977c6aa4db145f26a Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 11 Jun 2025 19:09:43 +0200 Subject: [PATCH 331/369] Feat: Database stats routes and more --- src/core/database/containerStats.ts | 66 +- src/core/database/database.ts | 69 +- src/core/database/dockerHosts.ts | 89 +-- src/core/database/hostStats.ts | 86 +- src/core/database/logs.ts | 110 +-- src/core/docker/store-host-stats.ts | 124 ++- src/index.ts | 294 +++---- src/routes/database-stats.ts | 169 ++++ src/routes/docker-stats.ts | 1132 +++++++++++++-------------- 9 files changed, 1167 insertions(+), 972 deletions(-) create mode 100644 src/routes/database-stats.ts diff --git a/src/core/database/containerStats.ts b/src/core/database/containerStats.ts index a5d6bcf0..ebbf390c 100644 --- a/src/core/database/containerStats.ts +++ b/src/core/database/containerStats.ts @@ -1,34 +1,52 @@ +import type { containerStatistics } from "~/typings/database"; import { db } from "./database"; import { executeDbOperation } from "./helper"; -const stmt = db.prepare(` +const insert = db.prepare(` INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); +const get = db.prepare("SELECT * FROM container_stats"); + export function addContainerStats( - id: string, - hostId: string, - name: string, - image: string, - status: string, - state: string, - cpu_usage: number, - memory_usage: number, + id: string, + hostId: string, + name: string, + image: string, + status: string, + state: string, + cpu_usage: number, + memory_usage: number ) { - return executeDbOperation( - "Add Container Stats", - () => - stmt.run(id, hostId, name, image, status, state, cpu_usage, memory_usage), - () => { - if ( - typeof id !== "string" || - typeof hostId !== "string" || - typeof cpu_usage !== "number" || - typeof memory_usage !== "number" - ) { - throw new TypeError("Invalid container stats parameters"); - } - }, - ); + return executeDbOperation( + "Add Container Stats", + () => + insert.run( + id, + hostId, + name, + image, + status, + state, + cpu_usage, + memory_usage + ), + () => { + if ( + typeof id !== "string" || + typeof hostId !== "string" || + typeof cpu_usage !== "number" || + typeof memory_usage !== "number" + ) { + throw new TypeError("Invalid container stats parameters"); + } + } + ); +} + +export function getContainerStats(): containerStatistics[] { + return executeDbOperation("Get Container Stats", () => + get.all() + ) as containerStatistics[]; } diff --git a/src/core/database/database.ts b/src/core/database/database.ts index 3ee6f54f..ae45c242 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -13,26 +13,26 @@ const uid = userInfo().uid; export let db: Database; try { - const databasePath = path.join(dataFolder, "dockstatapi.db"); - console.log("Database path:", databasePath); - console.log(`Running as: ${username} (${uid}:${gid})`); + const databasePath = path.join(dataFolder, "dockstatapi.db"); + console.log("Database path:", databasePath); + console.log(`Running as: ${username} (${uid}:${gid})`); - if (!existsSync(dataFolder)) { - await mkdir(dataFolder, { recursive: true, mode: 0o777 }); - console.log("Created data directory:", dataFolder); - } + if (!existsSync(dataFolder)) { + await mkdir(dataFolder, { recursive: true, mode: 0o777 }); + console.log("Created data directory:", dataFolder); + } - db = new Database(databasePath, { create: true }); - console.log("Database opened successfully"); + db = new Database(databasePath, { create: true }); + console.log("Database opened successfully"); - db.exec("PRAGMA journal_mode = WAL;"); + db.exec("PRAGMA journal_mode = WAL;"); } catch (error) { - console.error(`Cannot start DockStatAPI: ${error}`); - process.exit(500); + console.error(`Cannot start DockStatAPI: ${error}`); + process.exit(500); } export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -59,7 +59,7 @@ export function init() { ); CREATE TABLE IF NOT EXISTS host_stats ( - hostId INTEGER PRIMARY KEY NOT NULL, + hostId INTEGER NOT NULL, hostName TEXT NOT NULL, dockerVersion TEXT NOT NULL, apiVersion TEXT NOT NULL, @@ -72,7 +72,8 @@ export function init() { containersRunning INTEGER NOT NULL, containersStopped INTEGER NOT NULL, containersPaused INTEGER NOT NULL, - images INTEGER NOT NULL + images INTEGER NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS container_stats ( @@ -94,25 +95,25 @@ export function init() { ); `); - const configRow = db - .prepare("SELECT COUNT(*) AS count FROM config") - .get() as { count: number }; - - if (configRow.count === 0) { - db.prepare( - 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")', - ).run(); - } - - const hostRow = db - .prepare("SELECT COUNT(*) AS count FROM docker_hosts") - .get() as { count: number }; - - if (hostRow.count === 0) { - db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", - ).run("Localhost", "localhost:2375", false); - } + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; + + if (configRow.count === 0) { + db.prepare( + 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")' + ).run(); + } + + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; + + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" + ).run("Localhost", "localhost:2375", false); + } } init(); diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts index 2c9903db..a2fc2ca0 100644 --- a/src/core/database/dockerHosts.ts +++ b/src/core/database/dockerHosts.ts @@ -1,61 +1,62 @@ +import type { DockerHost } from "~/typings/docker"; import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", - ), - selectAll: db.prepare( - "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC", - ), - update: db.prepare( - "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?", - ), - delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), + insert: db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" + ), + selectAll: db.prepare( + "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC" + ), + update: db.prepare( + "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?" + ), + delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), }; export function addDockerHost(host: DockerHost) { - return executeDbOperation( - "Add Docker Host", - () => stmt.insert.run(host.name, host.hostAddress, host.secure), - () => { - if (!host.name || !host.hostAddress) - throw new Error("Missing required fields"); - if (typeof host.secure !== "boolean") - throw new TypeError("Invalid secure type"); - }, - ); + return executeDbOperation( + "Add Docker Host", + () => stmt.insert.run(host.name, host.hostAddress, host.secure), + () => { + if (!host.name || !host.hostAddress) + throw new Error("Missing required fields"); + if (typeof host.secure !== "boolean") + throw new TypeError("Invalid secure type"); + } + ); } export function getDockerHosts(): DockerHost[] { - return executeDbOperation("Get Docker Hosts", () => { - const rows = stmt.selectAll.all() as Array< - Omit & { secure: number } - >; - return rows.map((row) => ({ - ...row, - secure: row.secure === 1, - })); - }); + return executeDbOperation("Get Docker Hosts", () => { + const rows = stmt.selectAll.all() as Array< + Omit & { secure: number } + >; + return rows.map((row) => ({ + ...row, + secure: row.secure === 1, + })); + }); } 1; export function updateDockerHost(host: DockerHost) { - return executeDbOperation( - "Update Docker Host", - () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), - () => { - if (!host.id || typeof host.id !== "number") - throw new Error("Invalid host ID"); - }, - ); + return executeDbOperation( + "Update Docker Host", + () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), + () => { + if (!host.id || typeof host.id !== "number") + throw new Error("Invalid host ID"); + } + ); } export function deleteDockerHost(id: number) { - return executeDbOperation( - "Delete Docker Host", - () => stmt.delete.run(id), - () => { - if (typeof id !== "number") throw new TypeError("Invalid ID type"); - }, - ); + return executeDbOperation( + "Delete Docker Host", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid ID type"); + } + ); } diff --git a/src/core/database/hostStats.ts b/src/core/database/hostStats.ts index 3d48528d..e00f2a1a 100644 --- a/src/core/database/hostStats.ts +++ b/src/core/database/hostStats.ts @@ -2,44 +2,64 @@ import type { HostStats } from "~/typings/docker"; import { db } from "./database"; import { executeDbOperation } from "./helper"; -const stmt = db.prepare(` +const insert = db.prepare(` INSERT INTO host_stats ( hostId, hostName, dockerVersion, apiVersion, os, architecture, totalMemory, totalCPU, labels, containers, containersRunning, containersStopped, containersPaused, images ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(hostId) DO UPDATE SET - dockerVersion = excluded.dockerVersion, - apiVersion = excluded.apiVersion, - os = excluded.os, - architecture = excluded.architecture, - totalMemory = excluded.totalMemory, - totalCPU = excluded.totalCPU, - labels = excluded.labels, - containers = excluded.containers, - containersRunning = excluded.containersRunning, - containersStopped = excluded.containersStopped, - containersPaused = excluded.containersPaused, - images = excluded.images `); -export function updateHostStats(stats: HostStats) { - return executeDbOperation("Update Host Stats", () => - stmt.run( - stats.hostId, - stats.hostName, - stats.dockerVersion, - stats.apiVersion, - stats.os, - stats.architecture, - stats.totalMemory, - stats.totalCPU, - JSON.stringify(stats.labels), - stats.containers, - stats.containersRunning, - stats.containersStopped, - stats.containersPaused, - stats.images, - ), - ); +const selectStmt = db.prepare(` + SELECT * + FROM host_stats +`); + +export function addHostStats(stats: HostStats) { + return executeDbOperation( + "Update Host Stats", + () => + insert.run( + stats.hostId, + stats.hostName, + stats.dockerVersion, + stats.apiVersion, + stats.os, + stats.architecture, + stats.totalMemory, + stats.totalCPU, + JSON.stringify(stats.labels), + stats.containers, + stats.containersRunning, + stats.containersStopped, + stats.containersPaused, + stats.images + ), + () => { + if ( + typeof stats.hostId !== "number" || + typeof stats.hostName !== "string" || + typeof stats.dockerVersion !== "string" || + typeof stats.apiVersion !== "string" || + typeof stats.os !== "string" || + typeof stats.architecture !== "string" || + typeof stats.totalMemory !== "number" || + typeof stats.totalCPU !== "number" || + typeof JSON.stringify(stats.labels) !== "string" || + typeof stats.containers !== "number" || + typeof stats.containersRunning !== "number" || + typeof stats.containersStopped !== "number" || + typeof stats.containersPaused !== "number" || + typeof stats.images !== "number" + ) { + throw new TypeError(`Invalid Host Stats! - ${stats}`); + } + } + ); +} + +export function getHostStats(): HostStats[] { + return executeDbOperation("Get Host Stats", () => + selectStmt.all() + ) as HostStats[]; } diff --git a/src/core/database/logs.ts b/src/core/database/logs.ts index eb815d54..8ffbe814 100644 --- a/src/core/database/logs.ts +++ b/src/core/database/logs.ts @@ -3,77 +3,77 @@ import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare( - "INSERT INTO backend_log_entries (level, timestamp, message, file, line) VALUES (?, ?, ?, ?, ?)", - ), - selectAll: db.prepare( - "SELECT level, timestamp, message, file, line FROM backend_log_entries ORDER BY timestamp DESC", - ), - selectByLevel: db.prepare( - "SELECT level, timestamp, message, file, line FROM backend_log_entries WHERE level = ?", - ), - deleteAll: db.prepare("DELETE FROM backend_log_entries"), - deleteByLevel: db.prepare("DELETE FROM backend_log_entries WHERE level = ?"), + insert: db.prepare( + "INSERT INTO backend_log_entries (level, timestamp, message, file, line) VALUES (?, ?, ?, ?, ?)" + ), + selectAll: db.prepare( + "SELECT level, timestamp, message, file, line FROM backend_log_entries ORDER BY timestamp DESC" + ), + selectByLevel: db.prepare( + "SELECT level, timestamp, message, file, line FROM backend_log_entries WHERE level = ?" + ), + deleteAll: db.prepare("DELETE FROM backend_log_entries"), + deleteByLevel: db.prepare("DELETE FROM backend_log_entries WHERE level = ?"), }; function convertToLogMessage(row: log_message): log_message { - return { - level: row.level, - timestamp: row.timestamp, - message: row.message, - file: row.file, - line: row.line, - }; + return { + level: row.level, + timestamp: row.timestamp, + message: row.message, + file: row.file, + line: row.line, + }; } export function addLogEntry(data: log_message) { - return executeDbOperation( - "Add Log Entry", - () => - stmt.insert.run( - data.level, - data.timestamp, - data.message, - data.file, - data.line, - ), - () => { - if ( - typeof data.level !== "string" || - typeof data.timestamp !== "string" || - typeof data.message !== "string" || - typeof data.file !== "string" || - typeof data.line !== "number" - ) { - throw new TypeError( - "Invalid log entry parameters ${data.file} ${data.line} ${data.message} ${data}", - ); - } - }, - true, - ); + return executeDbOperation( + "Add Log Entry", + () => + stmt.insert.run( + data.level, + data.timestamp, + data.message, + data.file, + data.line + ), + () => { + if ( + typeof data.level !== "string" || + typeof data.timestamp !== "string" || + typeof data.message !== "string" || + typeof data.file !== "string" || + typeof data.line !== "number" + ) { + throw new TypeError( + `Invalid log entry parameters ${data.file} ${data.line} ${data.message} ${data}` + ); + } + }, + true + ); } export function getAllLogs(): log_message[] { - return executeDbOperation("Get All Logs", () => - stmt.selectAll.all().map((row) => convertToLogMessage(row as log_message)), - ); + return executeDbOperation("Get All Logs", () => + stmt.selectAll.all().map((row) => convertToLogMessage(row as log_message)) + ); } export function getLogsByLevel(level: string): log_message[] { - return executeDbOperation("Get Logs By Level", () => - stmt.selectByLevel - .all(level) - .map((row) => convertToLogMessage(row as log_message)), - ); + return executeDbOperation("Get Logs By Level", () => + stmt.selectByLevel + .all(level) + .map((row) => convertToLogMessage(row as log_message)) + ); } export function clearAllLogs() { - return executeDbOperation("Clear All Logs", () => stmt.deleteAll.run()); + return executeDbOperation("Clear All Logs", () => stmt.deleteAll.run()); } export function clearLogsByLevel(level: string) { - return executeDbOperation("Clear Logs By Level", () => - stmt.deleteByLevel.run(level), - ); + return executeDbOperation("Clear Logs By Level", () => + stmt.deleteByLevel.run(level) + ); } diff --git a/src/core/docker/store-host-stats.ts b/src/core/docker/store-host-stats.ts index 053f37ea..6dbdb9fe 100644 --- a/src/core/docker/store-host-stats.ts +++ b/src/core/docker/store-host-stats.ts @@ -1,84 +1,68 @@ import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; -import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; -import type { DockerHost, HostStats } from "~/typings/docker"; +import type { HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; -function getHostByName(hostName: string): DockerHost { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const foundHost = findObjectByKey(hosts, "name", hostName); - if (!foundHost) { - throw new Error(`Host ${hostName} not found`); - } - return foundHost; -} - async function storeHostData() { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - - await Promise.all( - hosts.map(async (host) => { - const docker = getDockerClient(host); - - try { - await docker.ping(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to ping docker host "${host.name}": ${errMsg}`, - ); - } + try { + const hosts = dbFunctions.getDockerHosts(); - let hostStats: DockerInfo; - try { - hostStats = await docker.info(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to fetch stats for host "${host.name}": ${errMsg}`, - ); - } + await Promise.all( + hosts.map(async (host) => { + const docker = getDockerClient(host); - const hostId = getHostByName(host.name).id; + try { + await docker.ping(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to ping docker host "${host.name}": ${errMsg}` + ); + } - if (!hostId) { - throw new Error(`Host "${host.name}" not found`); - } + let hostStats: DockerInfo; + try { + hostStats = await docker.info(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to fetch stats for host "${host.name}": ${errMsg}` + ); + } - try { - const stats: HostStats = { - hostId: hostId, - hostName: host.name, - dockerVersion: hostStats.ServerVersion, - apiVersion: hostStats.Driver, - os: hostStats.OperatingSystem, - architecture: hostStats.Architecture, - totalMemory: hostStats.MemTotal, - totalCPU: hostStats.NCPU, - labels: hostStats.Labels, - images: hostStats.Images, - containers: hostStats.Containers, - containersPaused: hostStats.ContainersPaused, - containersRunning: hostStats.ContainersRunning, - containersStopped: hostStats.ContainersStopped, - }; + try { + const stats: HostStats = { + hostId: host.id, + hostName: host.name, + dockerVersion: hostStats.ServerVersion, + apiVersion: hostStats.Driver, + os: hostStats.OperatingSystem, + architecture: hostStats.Architecture, + totalMemory: hostStats.MemTotal, + totalCPU: hostStats.NCPU, + labels: hostStats.Labels, + images: hostStats.Images, + containers: hostStats.Containers, + containersPaused: hostStats.ContainersPaused, + containersRunning: hostStats.ContainersRunning, + containersStopped: hostStats.ContainersStopped, + }; - dbFunctions.updateHostStats(stats); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to store stats for host "${host.name}": ${errMsg}`, - ); - } - }), - ); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - logger.error(`storeHostData failed: ${errMsg}`); - throw new Error(`Failed to store host data: ${errMsg}`); - } + dbFunctions.addHostStats(stats); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to store stats for host "${host.name}": ${errMsg}` + ); + } + }) + ); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + logger.error(`storeHostData failed: ${errMsg}`); + throw new Error(`Failed to store host data: ${errMsg}`); + } } export default storeHostData; diff --git a/src/index.ts b/src/index.ts index 5741cbdd..39dc6448 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,9 +11,9 @@ import { setSchedules } from "~/core/docker/scheduler"; import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { - authorWebsite, - contributors, - license, + authorWebsite, + contributors, + license, } from "~/core/utils/package-json"; import { swaggerReadme } from "~/core/utils/swagger-readme"; import { validateApiKey } from "~/middleware/auth"; @@ -27,158 +27,160 @@ import { stackRoutes } from "~/routes/stacks"; import type { config } from "~/typings/database"; import { checkStacks } from "./core/stacks/checker"; import { liveStacks } from "./routes/live-stacks"; +import { databaseStats } from "./routes/database-stats"; console.log(""); logger.info("Starting DockStatAPI"); const DockStatAPI = new Elysia({ - normalize: true, - precompile: true, + normalize: true, + precompile: true, }) - .use(cors()) - //.use(Logestic.preset("fancy")) - .use(staticPlugin()) - .use(serverTiming()) - .use( - dts("./src/index.ts", { - tsconfig: "./tsconfig.json", - compilerOptions: { - strict: true, - }, - }), - ) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey" as const, - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }), - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; - - if ( - path === "/health" || - path.startsWith("/swagger") || - path.startsWith("/public") - ) { - logger.info(`Requested unguarded route: ${path}`); - return; - } - - const validation = await validateApiKey(request, set); - - if (!validation) { - throw new Error("Error while checking API key"); - } - - if (!validation.success) { - set.status = 400; - - throw new Error(validation.error); - } - }) - .onError(({ code, set, path, error }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - - logger.error(`Internal server error at ${path}: ${error.message}`); - set.status = 500; - set.headers["Content-Type"] = "text/html"; - return { success: false, message: error.message }; - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(stackRoutes) - .use(liveLogs) - .use(liveStacks) - .get("/health", () => ({ status: "healthy" }), { - tags: ["Utils"], - response: { message: "healthy" }, - }) - .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }); + .use(cors()) + //.use(Logestic.preset("fancy")) + .use(staticPlugin()) + .use(serverTiming()) + .use( + dts("./src/index.ts", { + tsconfig: "./tsconfig.json", + compilerOptions: { + strict: true, + }, + }) + ) + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "3.0.0", + description: swaggerReadme, + }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey" as const, + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], + }, + }) + ) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; + + if ( + path === "/health" || + path.startsWith("/swagger") || + path.startsWith("/public") + ) { + logger.info(`Requested unguarded route: ${path}`); + return; + } + + const validation = await validateApiKey(request, set); + + if (!validation) { + throw new Error("Error while checking API key"); + } + + if (!validation.success) { + set.status = 400; + + throw new Error(validation.error); + } + }) + .onError(({ code, set, path, error }) => { + if (code === "NOT_FOUND") { + logger.warn(`Unknown route (${path}), showing error page!`); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); + } + + logger.error(`Internal server error at ${path}: ${error}`); + set.status = 500; + set.headers["Content-Type"] = "text/html"; + return { success: false, message: error }; + }) + .use(dockerRoutes) + .use(dockerStatsRoutes) + .use(backendLogs) + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .use(stackRoutes) + .use(liveLogs) + .use(liveStacks) + .use(databaseStats) + .get("/health", () => ({ status: "healthy" }), { + tags: ["Utils"], + response: { message: "healthy" }, + }) + .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger` + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + }); const initializeServer = async () => { - try { - await loadPlugins("./src/plugins"); - await setSchedules(); - - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); - - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; - - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", - ); - } - - await checkStacks(); - - logger.info("Started server"); - console.log("----- [ ############## ]"); - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } + try { + await loadPlugins("./src/plugins"); + await setSchedules(); + + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); + + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; + + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" + ); + } + + await checkStacks(); + + logger.info("Started server"); + console.log("----- [ ############## ]"); + } catch (error) { + logger.error("Error while starting server:", error); + process.exit(1); + } }; await initializeServer(); diff --git a/src/routes/database-stats.ts b/src/routes/database-stats.ts new file mode 100644 index 00000000..7e8a4304 --- /dev/null +++ b/src/routes/database-stats.ts @@ -0,0 +1,169 @@ +import Elysia from "elysia"; +import { dbFunctions } from "~/core/database"; + +export const databaseStats = new Elysia({ prefix: "/db-stats" }) + .get( + "/containers", + async () => { + return dbFunctions.getContainerStats(); + }, + { + detail: { + tags: ["Statistics"], + description: "Shows all stored metrics of containers", + responses: { + "200": { + description: "Successfully fetched Container Stats from the DB", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + example: + "0c1142d825a4104f45099e8297428cc7ef820319924aa9cf46739cf1c147cdae", + }, + hostId: { + type: "string", + example: "Localhost", + }, + name: { + type: "string", + example: "heimdall", + }, + image: { + type: "string", + example: "linuxserver/heimdall:latest", + }, + status: { + type: "string", + example: "Up About a minute", + }, + state: { + type: "string", + example: "running", + }, + cpu_usage: { + type: "number", + example: 0.00628140703517588, + }, + memory_usage: { + type: "number", + example: 0.2784590652462969, + }, + timestamp: { + type: "string", + example: "2025-06-07 07:01:26", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/hosts", + async () => { + return dbFunctions.getHostStats(); + }, + { + detail: { + tags: ["Statistics"], + description: "Shows all stored metrics of Docker hosts", + responses: { + "200": { + description: "Successfully fetched Host Stats from the DB", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + description: "Unique identifier for the host", + }, + hostName: { + type: "string", + example: "Localhost", + description: "Display name of the host", + }, + dockerVersion: { + type: "string", + example: "28.2.0", + description: "Installed Docker version", + }, + apiVersion: { + type: "string", + example: "overlay2", + description: "Docker API version", + }, + os: { + type: "string", + example: "Arch Linux", + description: "Host operating system", + }, + architecture: { + type: "string", + example: "x86_64", + description: "System architecture", + }, + totalMemory: { + type: "number", + example: 33512706048, + description: "Total system memory in bytes", + }, + totalCPU: { + type: "number", + example: 4, + description: "Number of available CPU cores", + }, + labels: { + type: "string", + example: "[]", + description: "JSON string of host labels", + }, + containers: { + type: "number", + example: 3, + description: "Total containers on host", + }, + containersRunning: { + type: "number", + example: 3, + description: "Currently running containers", + }, + containersStopped: { + type: "number", + example: 0, + description: "Stopped containers", + }, + containersPaused: { + type: "number", + example: 0, + description: "Paused containers", + }, + images: { + type: "number", + example: 30, + description: "Available Docker images", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index aa968d2d..f9d966d6 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -3,8 +3,8 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; @@ -13,586 +13,586 @@ import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get( - "/containers", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; + .get( + "/containers", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - return responseHandler.error( - set, - pingError as string, - "Docker host connection failed", - ); - } + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + return responseHandler.error( + set, + pingError as string, + "Docker host connection failed" + ); + } - const hostContainers = await docker.listContainers({ all: true }); + const hostContainers = await docker.listContainers({ all: true }); - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - return responseHandler.reject( - set, - reject, - "An error occurred", - error, - ); - } - if (!stats) { - return responseHandler.reject( - set, - reject, - "No stats available", - ); - } - resolve(stats); - }); - }, - ); + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return responseHandler.reject( + set, + reject, + "An error occurred", + error + ); + } + if (!stats) { + return responseHandler.reject( + set, + reject, + "No stats available" + ); + } + resolve(stats); + }); + } + ); - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError, - ); - } - }), - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }), - ); + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError + ); + } + }) + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }) + ); - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", - responses: { - "200": { - description: "Successfully retrieved container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - containers: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "string", - example: "abc123def456", - }, - hostId: { - type: "string", - example: "1", - }, - name: { - type: "string", - example: "example-container", - }, - image: { - type: "string", - example: "nginx:latest", - }, - status: { - type: "string", - example: "running", - }, - state: { - type: "string", - example: "running", - }, - cpuUsage: { - type: "number", - example: 0.5, - }, - memoryUsage: { - type: "number", - example: 1024, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve containers", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", + responses: { + "200": { + description: "Successfully retrieved container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + containers: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + example: "abc123def456", + }, + hostId: { + type: "string", + example: "1", + }, + name: { + type: "string", + example: "example-container", + }, + image: { + type: "string", + example: "nginx:latest", + }, + status: { + type: "string", + example: "running", + }, + state: { + type: "string", + example: "running", + }, + cpuUsage: { + type: "number", + example: 0.5, + }, + memoryUsage: { + type: "number", + example: 1024, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve containers", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - logger.debug("Fetched all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - logger.debug("Fetched stats for all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for all hosts", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/hosts/:id", - async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched stats for all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for all hosts", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/hosts/:id", + async ({ params, set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = findObjectByKey(hosts, "id", Number(params.id)); - if (!host) { - return responseHandler.simple_error( - set, - `Host (${params.id}) not found`, - ); - } + const host = findObjectByKey(hosts, "id", Number(params.id)); + if (!host) { + return responseHandler.simple_error( + set, + `Host (${params.id}) not found` + ); + } - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config" + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ); From 47900c045959e6e47dfc410dd15fdf501e92e260 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 13 Jun 2025 21:18:00 +0200 Subject: [PATCH 332/369] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20dependenc?= =?UTF-8?q?ies=20and=20ignore=20.gitai.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit updates `dockerode` to version 4.0.7 and adds `.gitai.json` to `.gitignore` and `.dockerignore` to prevent it from being included in the Docker image or tracked by Git. It also includes updates to other dependencies and devDependencies. --- .dockerignore | 3 ++- .gitignore | 1 + CHANGELOG.md | 20 ++++++++++++++++++++ package.json | 7 ++++--- 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.dockerignore b/.dockerignore index e6dbc5af..14170b78 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,4 +9,5 @@ *.lock src/tests .github -.local-tests \ No newline at end of file +.local-tests +.gitai.json diff --git a/.gitignore b/.gitignore index e34b9b1e..d75865c7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build dependency-*.{mmd,dot,svg} Knip-Report.md reports/** +.gitai.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..28db443c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +## Changelog + +### Added + +* **Database Stats Routes and More** - (Its4Nik, 2025-06-11) - (ef74b6a64d91d0683a9962e977c6aa4db145f26a) +* **Stack Validation** - (Its4Nik, 2025-06-02) - (871ab4f92dee49569d9028490442b2537d265430) +* **Unknown Changes** - (Its4Nik, 2025-06-02) - (3a9a3dc8d37c0eea1b7af67b18176b8209989dcb) - *Note: Description indicates significant but unspecified changes.* + +### Changed + +* **Dependency Graphs Updated** - (Its4Nik, 2025-06-07) - (ffab878a9d536af7aa14dd7b785643f50ac973ec) +* **Stacks Controller Refactor** - (Its4Nik, 2025-06-06) - (e0ed2557ecfdc546786fb4969e8dec602a35708b) + +### Fixed + +* **Linter Fixes** - (Its4Nik, 2025-06-02) - (ec0c79abe3f2f55fe80b220305eb96f19f03c173) +* **Minor Refactor and Bug Fixes** - (Its4Nik, 2025-06-07) - (1177e7e5d90d0f675719b60c669f05f2871024ca) +* **UT: Fix Wrong Body Selection** - (Its4Nik, 2025-05-16) - (4afe4ca05c999da6d70cea95b7d4ea871dcf0723) +* **UT: Fix Wrong Body Selection** - (Its4Nik, 2025-05-16) - (2e8528ab8a500a925f3411582a21fbeccc683262) +* **CQL: Apply Lint Fixes** - (GitHub Actions, 2025-05-16) - (7bb328b0f4a8770130e71eddc419bd926b7af90f) \ No newline at end of file diff --git a/package.json b/package.json index 118ba2b5..4d4f4bb1 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "chalk": "^5.4.1", "date-fns": "^4.1.0", "docker-compose": "^1.2.0", - "dockerode": "^4.0.6", + "dockerode": "^4.0.7", "elysia": "latest", "elysia-remote-dts": "^1.0.3", "js-yaml": "^4.1.0", @@ -44,10 +44,11 @@ }, "devDependencies": { "@biomejs/biome": "1.9.4", + "@its_4_nik/gitai": "^1.0.10", "@types/bun": "latest", - "@types/dockerode": "^3.3.39", + "@types/dockerode": "^3.3.40", "@types/js-yaml": "^4.0.9", - "@types/node": "^22.15.29", + "@types/node": "^22.15.31", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", From 93f3f79778e9d8b7a21fab144c9a9acc3c37de48 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 13 Jun 2025 19:18:17 +0000 Subject: [PATCH 333/369] CQL: Apply lint fixes [skip ci] --- src/core/database/containerStats.ts | 70 +- src/core/database/database.ts | 64 +- src/core/database/dockerHosts.ts | 88 +-- src/core/database/hostStats.ts | 86 +- src/core/database/logs.ts | 110 +-- src/core/docker/store-host-stats.ts | 106 +-- src/index.ts | 296 +++---- src/routes/database-stats.ts | 330 ++++---- src/routes/docker-stats.ts | 1132 +++++++++++++-------------- 9 files changed, 1141 insertions(+), 1141 deletions(-) diff --git a/src/core/database/containerStats.ts b/src/core/database/containerStats.ts index ebbf390c..a50ea4c2 100644 --- a/src/core/database/containerStats.ts +++ b/src/core/database/containerStats.ts @@ -10,43 +10,43 @@ const insert = db.prepare(` const get = db.prepare("SELECT * FROM container_stats"); export function addContainerStats( - id: string, - hostId: string, - name: string, - image: string, - status: string, - state: string, - cpu_usage: number, - memory_usage: number + id: string, + hostId: string, + name: string, + image: string, + status: string, + state: string, + cpu_usage: number, + memory_usage: number, ) { - return executeDbOperation( - "Add Container Stats", - () => - insert.run( - id, - hostId, - name, - image, - status, - state, - cpu_usage, - memory_usage - ), - () => { - if ( - typeof id !== "string" || - typeof hostId !== "string" || - typeof cpu_usage !== "number" || - typeof memory_usage !== "number" - ) { - throw new TypeError("Invalid container stats parameters"); - } - } - ); + return executeDbOperation( + "Add Container Stats", + () => + insert.run( + id, + hostId, + name, + image, + status, + state, + cpu_usage, + memory_usage, + ), + () => { + if ( + typeof id !== "string" || + typeof hostId !== "string" || + typeof cpu_usage !== "number" || + typeof memory_usage !== "number" + ) { + throw new TypeError("Invalid container stats parameters"); + } + }, + ); } export function getContainerStats(): containerStatistics[] { - return executeDbOperation("Get Container Stats", () => - get.all() - ) as containerStatistics[]; + return executeDbOperation("Get Container Stats", () => + get.all(), + ) as containerStatistics[]; } diff --git a/src/core/database/database.ts b/src/core/database/database.ts index ae45c242..204666ef 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -13,26 +13,26 @@ const uid = userInfo().uid; export let db: Database; try { - const databasePath = path.join(dataFolder, "dockstatapi.db"); - console.log("Database path:", databasePath); - console.log(`Running as: ${username} (${uid}:${gid})`); + const databasePath = path.join(dataFolder, "dockstatapi.db"); + console.log("Database path:", databasePath); + console.log(`Running as: ${username} (${uid}:${gid})`); - if (!existsSync(dataFolder)) { - await mkdir(dataFolder, { recursive: true, mode: 0o777 }); - console.log("Created data directory:", dataFolder); - } + if (!existsSync(dataFolder)) { + await mkdir(dataFolder, { recursive: true, mode: 0o777 }); + console.log("Created data directory:", dataFolder); + } - db = new Database(databasePath, { create: true }); - console.log("Database opened successfully"); + db = new Database(databasePath, { create: true }); + console.log("Database opened successfully"); - db.exec("PRAGMA journal_mode = WAL;"); + db.exec("PRAGMA journal_mode = WAL;"); } catch (error) { - console.error(`Cannot start DockStatAPI: ${error}`); - process.exit(500); + console.error(`Cannot start DockStatAPI: ${error}`); + process.exit(500); } export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -95,25 +95,25 @@ export function init() { ); `); - const configRow = db - .prepare("SELECT COUNT(*) AS count FROM config") - .get() as { count: number }; - - if (configRow.count === 0) { - db.prepare( - 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")' - ).run(); - } - - const hostRow = db - .prepare("SELECT COUNT(*) AS count FROM docker_hosts") - .get() as { count: number }; - - if (hostRow.count === 0) { - db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" - ).run("Localhost", "localhost:2375", false); - } + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; + + if (configRow.count === 0) { + db.prepare( + 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")', + ).run(); + } + + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; + + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ).run("Localhost", "localhost:2375", false); + } } init(); diff --git a/src/core/database/dockerHosts.ts b/src/core/database/dockerHosts.ts index a2fc2ca0..18180c54 100644 --- a/src/core/database/dockerHosts.ts +++ b/src/core/database/dockerHosts.ts @@ -3,60 +3,60 @@ import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)" - ), - selectAll: db.prepare( - "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC" - ), - update: db.prepare( - "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?" - ), - delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), + insert: db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ), + selectAll: db.prepare( + "SELECT id, name, hostAddress, secure FROM docker_hosts ORDER BY id DESC", + ), + update: db.prepare( + "UPDATE docker_hosts SET hostAddress = ?, secure = ?, name = ? WHERE id = ?", + ), + delete: db.prepare("DELETE FROM docker_hosts WHERE id = ?"), }; export function addDockerHost(host: DockerHost) { - return executeDbOperation( - "Add Docker Host", - () => stmt.insert.run(host.name, host.hostAddress, host.secure), - () => { - if (!host.name || !host.hostAddress) - throw new Error("Missing required fields"); - if (typeof host.secure !== "boolean") - throw new TypeError("Invalid secure type"); - } - ); + return executeDbOperation( + "Add Docker Host", + () => stmt.insert.run(host.name, host.hostAddress, host.secure), + () => { + if (!host.name || !host.hostAddress) + throw new Error("Missing required fields"); + if (typeof host.secure !== "boolean") + throw new TypeError("Invalid secure type"); + }, + ); } export function getDockerHosts(): DockerHost[] { - return executeDbOperation("Get Docker Hosts", () => { - const rows = stmt.selectAll.all() as Array< - Omit & { secure: number } - >; - return rows.map((row) => ({ - ...row, - secure: row.secure === 1, - })); - }); + return executeDbOperation("Get Docker Hosts", () => { + const rows = stmt.selectAll.all() as Array< + Omit & { secure: number } + >; + return rows.map((row) => ({ + ...row, + secure: row.secure === 1, + })); + }); } 1; export function updateDockerHost(host: DockerHost) { - return executeDbOperation( - "Update Docker Host", - () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), - () => { - if (!host.id || typeof host.id !== "number") - throw new Error("Invalid host ID"); - } - ); + return executeDbOperation( + "Update Docker Host", + () => stmt.update.run(host.hostAddress, host.secure, host.name, host.id), + () => { + if (!host.id || typeof host.id !== "number") + throw new Error("Invalid host ID"); + }, + ); } export function deleteDockerHost(id: number) { - return executeDbOperation( - "Delete Docker Host", - () => stmt.delete.run(id), - () => { - if (typeof id !== "number") throw new TypeError("Invalid ID type"); - } - ); + return executeDbOperation( + "Delete Docker Host", + () => stmt.delete.run(id), + () => { + if (typeof id !== "number") throw new TypeError("Invalid ID type"); + }, + ); } diff --git a/src/core/database/hostStats.ts b/src/core/database/hostStats.ts index e00f2a1a..49e6ce93 100644 --- a/src/core/database/hostStats.ts +++ b/src/core/database/hostStats.ts @@ -16,50 +16,50 @@ const selectStmt = db.prepare(` `); export function addHostStats(stats: HostStats) { - return executeDbOperation( - "Update Host Stats", - () => - insert.run( - stats.hostId, - stats.hostName, - stats.dockerVersion, - stats.apiVersion, - stats.os, - stats.architecture, - stats.totalMemory, - stats.totalCPU, - JSON.stringify(stats.labels), - stats.containers, - stats.containersRunning, - stats.containersStopped, - stats.containersPaused, - stats.images - ), - () => { - if ( - typeof stats.hostId !== "number" || - typeof stats.hostName !== "string" || - typeof stats.dockerVersion !== "string" || - typeof stats.apiVersion !== "string" || - typeof stats.os !== "string" || - typeof stats.architecture !== "string" || - typeof stats.totalMemory !== "number" || - typeof stats.totalCPU !== "number" || - typeof JSON.stringify(stats.labels) !== "string" || - typeof stats.containers !== "number" || - typeof stats.containersRunning !== "number" || - typeof stats.containersStopped !== "number" || - typeof stats.containersPaused !== "number" || - typeof stats.images !== "number" - ) { - throw new TypeError(`Invalid Host Stats! - ${stats}`); - } - } - ); + return executeDbOperation( + "Update Host Stats", + () => + insert.run( + stats.hostId, + stats.hostName, + stats.dockerVersion, + stats.apiVersion, + stats.os, + stats.architecture, + stats.totalMemory, + stats.totalCPU, + JSON.stringify(stats.labels), + stats.containers, + stats.containersRunning, + stats.containersStopped, + stats.containersPaused, + stats.images, + ), + () => { + if ( + typeof stats.hostId !== "number" || + typeof stats.hostName !== "string" || + typeof stats.dockerVersion !== "string" || + typeof stats.apiVersion !== "string" || + typeof stats.os !== "string" || + typeof stats.architecture !== "string" || + typeof stats.totalMemory !== "number" || + typeof stats.totalCPU !== "number" || + typeof JSON.stringify(stats.labels) !== "string" || + typeof stats.containers !== "number" || + typeof stats.containersRunning !== "number" || + typeof stats.containersStopped !== "number" || + typeof stats.containersPaused !== "number" || + typeof stats.images !== "number" + ) { + throw new TypeError(`Invalid Host Stats! - ${stats}`); + } + }, + ); } export function getHostStats(): HostStats[] { - return executeDbOperation("Get Host Stats", () => - selectStmt.all() - ) as HostStats[]; + return executeDbOperation("Get Host Stats", () => + selectStmt.all(), + ) as HostStats[]; } diff --git a/src/core/database/logs.ts b/src/core/database/logs.ts index 8ffbe814..e6c30d1a 100644 --- a/src/core/database/logs.ts +++ b/src/core/database/logs.ts @@ -3,77 +3,77 @@ import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare( - "INSERT INTO backend_log_entries (level, timestamp, message, file, line) VALUES (?, ?, ?, ?, ?)" - ), - selectAll: db.prepare( - "SELECT level, timestamp, message, file, line FROM backend_log_entries ORDER BY timestamp DESC" - ), - selectByLevel: db.prepare( - "SELECT level, timestamp, message, file, line FROM backend_log_entries WHERE level = ?" - ), - deleteAll: db.prepare("DELETE FROM backend_log_entries"), - deleteByLevel: db.prepare("DELETE FROM backend_log_entries WHERE level = ?"), + insert: db.prepare( + "INSERT INTO backend_log_entries (level, timestamp, message, file, line) VALUES (?, ?, ?, ?, ?)", + ), + selectAll: db.prepare( + "SELECT level, timestamp, message, file, line FROM backend_log_entries ORDER BY timestamp DESC", + ), + selectByLevel: db.prepare( + "SELECT level, timestamp, message, file, line FROM backend_log_entries WHERE level = ?", + ), + deleteAll: db.prepare("DELETE FROM backend_log_entries"), + deleteByLevel: db.prepare("DELETE FROM backend_log_entries WHERE level = ?"), }; function convertToLogMessage(row: log_message): log_message { - return { - level: row.level, - timestamp: row.timestamp, - message: row.message, - file: row.file, - line: row.line, - }; + return { + level: row.level, + timestamp: row.timestamp, + message: row.message, + file: row.file, + line: row.line, + }; } export function addLogEntry(data: log_message) { - return executeDbOperation( - "Add Log Entry", - () => - stmt.insert.run( - data.level, - data.timestamp, - data.message, - data.file, - data.line - ), - () => { - if ( - typeof data.level !== "string" || - typeof data.timestamp !== "string" || - typeof data.message !== "string" || - typeof data.file !== "string" || - typeof data.line !== "number" - ) { - throw new TypeError( - `Invalid log entry parameters ${data.file} ${data.line} ${data.message} ${data}` - ); - } - }, - true - ); + return executeDbOperation( + "Add Log Entry", + () => + stmt.insert.run( + data.level, + data.timestamp, + data.message, + data.file, + data.line, + ), + () => { + if ( + typeof data.level !== "string" || + typeof data.timestamp !== "string" || + typeof data.message !== "string" || + typeof data.file !== "string" || + typeof data.line !== "number" + ) { + throw new TypeError( + `Invalid log entry parameters ${data.file} ${data.line} ${data.message} ${data}`, + ); + } + }, + true, + ); } export function getAllLogs(): log_message[] { - return executeDbOperation("Get All Logs", () => - stmt.selectAll.all().map((row) => convertToLogMessage(row as log_message)) - ); + return executeDbOperation("Get All Logs", () => + stmt.selectAll.all().map((row) => convertToLogMessage(row as log_message)), + ); } export function getLogsByLevel(level: string): log_message[] { - return executeDbOperation("Get Logs By Level", () => - stmt.selectByLevel - .all(level) - .map((row) => convertToLogMessage(row as log_message)) - ); + return executeDbOperation("Get Logs By Level", () => + stmt.selectByLevel + .all(level) + .map((row) => convertToLogMessage(row as log_message)), + ); } export function clearAllLogs() { - return executeDbOperation("Clear All Logs", () => stmt.deleteAll.run()); + return executeDbOperation("Clear All Logs", () => stmt.deleteAll.run()); } export function clearLogsByLevel(level: string) { - return executeDbOperation("Clear Logs By Level", () => - stmt.deleteByLevel.run(level) - ); + return executeDbOperation("Clear Logs By Level", () => + stmt.deleteByLevel.run(level), + ); } diff --git a/src/core/docker/store-host-stats.ts b/src/core/docker/store-host-stats.ts index 6dbdb9fe..815b9db5 100644 --- a/src/core/docker/store-host-stats.ts +++ b/src/core/docker/store-host-stats.ts @@ -5,64 +5,64 @@ import type { HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; async function storeHostData() { - try { - const hosts = dbFunctions.getDockerHosts(); + try { + const hosts = dbFunctions.getDockerHosts(); - await Promise.all( - hosts.map(async (host) => { - const docker = getDockerClient(host); + await Promise.all( + hosts.map(async (host) => { + const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to ping docker host "${host.name}": ${errMsg}` - ); - } + try { + await docker.ping(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to ping docker host "${host.name}": ${errMsg}`, + ); + } - let hostStats: DockerInfo; - try { - hostStats = await docker.info(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to fetch stats for host "${host.name}": ${errMsg}` - ); - } + let hostStats: DockerInfo; + try { + hostStats = await docker.info(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to fetch stats for host "${host.name}": ${errMsg}`, + ); + } - try { - const stats: HostStats = { - hostId: host.id, - hostName: host.name, - dockerVersion: hostStats.ServerVersion, - apiVersion: hostStats.Driver, - os: hostStats.OperatingSystem, - architecture: hostStats.Architecture, - totalMemory: hostStats.MemTotal, - totalCPU: hostStats.NCPU, - labels: hostStats.Labels, - images: hostStats.Images, - containers: hostStats.Containers, - containersPaused: hostStats.ContainersPaused, - containersRunning: hostStats.ContainersRunning, - containersStopped: hostStats.ContainersStopped, - }; + try { + const stats: HostStats = { + hostId: host.id, + hostName: host.name, + dockerVersion: hostStats.ServerVersion, + apiVersion: hostStats.Driver, + os: hostStats.OperatingSystem, + architecture: hostStats.Architecture, + totalMemory: hostStats.MemTotal, + totalCPU: hostStats.NCPU, + labels: hostStats.Labels, + images: hostStats.Images, + containers: hostStats.Containers, + containersPaused: hostStats.ContainersPaused, + containersRunning: hostStats.ContainersRunning, + containersStopped: hostStats.ContainersStopped, + }; - dbFunctions.addHostStats(stats); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to store stats for host "${host.name}": ${errMsg}` - ); - } - }) - ); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - logger.error(`storeHostData failed: ${errMsg}`); - throw new Error(`Failed to store host data: ${errMsg}`); - } + dbFunctions.addHostStats(stats); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error( + `Failed to store stats for host "${host.name}": ${errMsg}`, + ); + } + }), + ); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + logger.error(`storeHostData failed: ${errMsg}`); + throw new Error(`Failed to store host data: ${errMsg}`); + } } export default storeHostData; diff --git a/src/index.ts b/src/index.ts index 39dc6448..c8f62b60 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,9 +11,9 @@ import { setSchedules } from "~/core/docker/scheduler"; import { loadPlugins } from "~/core/plugins/loader"; import { logger } from "~/core/utils/logger"; import { - authorWebsite, - contributors, - license, + authorWebsite, + contributors, + license, } from "~/core/utils/package-json"; import { swaggerReadme } from "~/core/utils/swagger-readme"; import { validateApiKey } from "~/middleware/auth"; @@ -26,161 +26,161 @@ import { backendLogs } from "~/routes/logs"; import { stackRoutes } from "~/routes/stacks"; import type { config } from "~/typings/database"; import { checkStacks } from "./core/stacks/checker"; -import { liveStacks } from "./routes/live-stacks"; import { databaseStats } from "./routes/database-stats"; +import { liveStacks } from "./routes/live-stacks"; console.log(""); logger.info("Starting DockStatAPI"); const DockStatAPI = new Elysia({ - normalize: true, - precompile: true, + normalize: true, + precompile: true, }) - .use(cors()) - //.use(Logestic.preset("fancy")) - .use(staticPlugin()) - .use(serverTiming()) - .use( - dts("./src/index.ts", { - tsconfig: "./tsconfig.json", - compilerOptions: { - strict: true, - }, - }) - ) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey" as const, - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }) - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; - - if ( - path === "/health" || - path.startsWith("/swagger") || - path.startsWith("/public") - ) { - logger.info(`Requested unguarded route: ${path}`); - return; - } - - const validation = await validateApiKey(request, set); - - if (!validation) { - throw new Error("Error while checking API key"); - } - - if (!validation.success) { - set.status = 400; - - throw new Error(validation.error); - } - }) - .onError(({ code, set, path, error }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - - logger.error(`Internal server error at ${path}: ${error}`); - set.status = 500; - set.headers["Content-Type"] = "text/html"; - return { success: false, message: error }; - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(stackRoutes) - .use(liveLogs) - .use(liveStacks) - .use(databaseStats) - .get("/health", () => ({ status: "healthy" }), { - tags: ["Utils"], - response: { message: "healthy" }, - }) - .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger` - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }); + .use(cors()) + //.use(Logestic.preset("fancy")) + .use(staticPlugin()) + .use(serverTiming()) + .use( + dts("./src/index.ts", { + tsconfig: "./tsconfig.json", + compilerOptions: { + strict: true, + }, + }), + ) + .use( + swagger({ + documentation: { + info: { + title: "DockStatAPI", + version: "3.0.0", + description: swaggerReadme, + }, + components: { + securitySchemes: { + apiKeyAuth: { + type: "apiKey" as const, + name: "x-api-key", + in: "header", + description: "API key for authentication", + }, + }, + }, + security: [ + { + apiKeyAuth: [], + }, + ], + tags: [ + { + name: "Statistics", + description: + "All endpoints for fetching statistics of hosts / containers", + }, + { + name: "Management", + description: "Various endpoints for managing DockStatAPI", + }, + { + name: "Stacks", + description: "DockStat's Stack functionality", + }, + { + name: "Utils", + description: "Various utilities which might be useful", + }, + ], + }, + }), + ) + .onBeforeHandle(async (context) => { + const { path, request, set } = context; + + if ( + path === "/health" || + path.startsWith("/swagger") || + path.startsWith("/public") + ) { + logger.info(`Requested unguarded route: ${path}`); + return; + } + + const validation = await validateApiKey(request, set); + + if (!validation) { + throw new Error("Error while checking API key"); + } + + if (!validation.success) { + set.status = 400; + + throw new Error(validation.error); + } + }) + .onError(({ code, set, path, error }) => { + if (code === "NOT_FOUND") { + logger.warn(`Unknown route (${path}), showing error page!`); + set.status = 404; + set.headers["Content-Type"] = "text/html"; + return Bun.file("public/404.html"); + } + + logger.error(`Internal server error at ${path}: ${error}`); + set.status = 500; + set.headers["Content-Type"] = "text/html"; + return { success: false, message: error }; + }) + .use(dockerRoutes) + .use(dockerStatsRoutes) + .use(backendLogs) + .use(dockerWebsocketRoutes) + .use(apiConfigRoutes) + .use(stackRoutes) + .use(liveLogs) + .use(liveStacks) + .use(databaseStats) + .get("/health", () => ({ status: "healthy" }), { + tags: ["Utils"], + response: { message: "healthy" }, + }) + .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { + console.log("----- [ ############## ]"); + logger.info(`DockStatAPI is running at http://${hostname}:${port}`); + logger.info( + `Swagger API Documentation available at http://${hostname}:${port}/swagger`, + ); + logger.info(`License: ${license}`); + logger.info(`Author: ${authorWebsite}`); + logger.info(`Contributors: ${contributors}`); + }); const initializeServer = async () => { - try { - await loadPlugins("./src/plugins"); - await setSchedules(); - - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); - - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; - - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!" - ); - } - - await checkStacks(); - - logger.info("Started server"); - console.log("----- [ ############## ]"); - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } + try { + await loadPlugins("./src/plugins"); + await setSchedules(); + + monitorDockerEvents().catch((error) => { + logger.error(`Monitoring Error: ${error}`); + }); + + const configData = dbFunctions.getConfig() as config[]; + const apiKey = configData[0].api_key; + + if (apiKey === "changeme") { + logger.warn( + "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", + ); + } + + await checkStacks(); + + logger.info("Started server"); + console.log("----- [ ############## ]"); + } catch (error) { + logger.error("Error while starting server:", error); + process.exit(1); + } }; await initializeServer(); diff --git a/src/routes/database-stats.ts b/src/routes/database-stats.ts index 7e8a4304..8a1bd882 100644 --- a/src/routes/database-stats.ts +++ b/src/routes/database-stats.ts @@ -2,168 +2,168 @@ import Elysia from "elysia"; import { dbFunctions } from "~/core/database"; export const databaseStats = new Elysia({ prefix: "/db-stats" }) - .get( - "/containers", - async () => { - return dbFunctions.getContainerStats(); - }, - { - detail: { - tags: ["Statistics"], - description: "Shows all stored metrics of containers", - responses: { - "200": { - description: "Successfully fetched Container Stats from the DB", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "string", - example: - "0c1142d825a4104f45099e8297428cc7ef820319924aa9cf46739cf1c147cdae", - }, - hostId: { - type: "string", - example: "Localhost", - }, - name: { - type: "string", - example: "heimdall", - }, - image: { - type: "string", - example: "linuxserver/heimdall:latest", - }, - status: { - type: "string", - example: "Up About a minute", - }, - state: { - type: "string", - example: "running", - }, - cpu_usage: { - type: "number", - example: 0.00628140703517588, - }, - memory_usage: { - type: "number", - example: 0.2784590652462969, - }, - timestamp: { - type: "string", - example: "2025-06-07 07:01:26", - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/hosts", - async () => { - return dbFunctions.getHostStats(); - }, - { - detail: { - tags: ["Statistics"], - description: "Shows all stored metrics of Docker hosts", - responses: { - "200": { - description: "Successfully fetched Host Stats from the DB", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - description: "Unique identifier for the host", - }, - hostName: { - type: "string", - example: "Localhost", - description: "Display name of the host", - }, - dockerVersion: { - type: "string", - example: "28.2.0", - description: "Installed Docker version", - }, - apiVersion: { - type: "string", - example: "overlay2", - description: "Docker API version", - }, - os: { - type: "string", - example: "Arch Linux", - description: "Host operating system", - }, - architecture: { - type: "string", - example: "x86_64", - description: "System architecture", - }, - totalMemory: { - type: "number", - example: 33512706048, - description: "Total system memory in bytes", - }, - totalCPU: { - type: "number", - example: 4, - description: "Number of available CPU cores", - }, - labels: { - type: "string", - example: "[]", - description: "JSON string of host labels", - }, - containers: { - type: "number", - example: 3, - description: "Total containers on host", - }, - containersRunning: { - type: "number", - example: 3, - description: "Currently running containers", - }, - containersStopped: { - type: "number", - example: 0, - description: "Stopped containers", - }, - containersPaused: { - type: "number", - example: 0, - description: "Paused containers", - }, - images: { - type: "number", - example: 30, - description: "Available Docker images", - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - ); + .get( + "/containers", + async () => { + return dbFunctions.getContainerStats(); + }, + { + detail: { + tags: ["Statistics"], + description: "Shows all stored metrics of containers", + responses: { + "200": { + description: "Successfully fetched Container Stats from the DB", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + example: + "0c1142d825a4104f45099e8297428cc7ef820319924aa9cf46739cf1c147cdae", + }, + hostId: { + type: "string", + example: "Localhost", + }, + name: { + type: "string", + example: "heimdall", + }, + image: { + type: "string", + example: "linuxserver/heimdall:latest", + }, + status: { + type: "string", + example: "Up About a minute", + }, + state: { + type: "string", + example: "running", + }, + cpu_usage: { + type: "number", + example: 0.00628140703517588, + }, + memory_usage: { + type: "number", + example: 0.2784590652462969, + }, + timestamp: { + type: "string", + example: "2025-06-07 07:01:26", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts", + async () => { + return dbFunctions.getHostStats(); + }, + { + detail: { + tags: ["Statistics"], + description: "Shows all stored metrics of Docker hosts", + responses: { + "200": { + description: "Successfully fetched Host Stats from the DB", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + description: "Unique identifier for the host", + }, + hostName: { + type: "string", + example: "Localhost", + description: "Display name of the host", + }, + dockerVersion: { + type: "string", + example: "28.2.0", + description: "Installed Docker version", + }, + apiVersion: { + type: "string", + example: "overlay2", + description: "Docker API version", + }, + os: { + type: "string", + example: "Arch Linux", + description: "Host operating system", + }, + architecture: { + type: "string", + example: "x86_64", + description: "System architecture", + }, + totalMemory: { + type: "number", + example: 33512706048, + description: "Total system memory in bytes", + }, + totalCPU: { + type: "number", + example: 4, + description: "Number of available CPU cores", + }, + labels: { + type: "string", + example: "[]", + description: "JSON string of host labels", + }, + containers: { + type: "number", + example: 3, + description: "Total containers on host", + }, + containersRunning: { + type: "number", + example: 3, + description: "Currently running containers", + }, + containersStopped: { + type: "number", + example: 0, + description: "Stopped containers", + }, + containersPaused: { + type: "number", + example: 0, + description: "Paused containers", + }, + images: { + type: "number", + example: 30, + description: "Available Docker images", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts index f9d966d6..aa968d2d 100644 --- a/src/routes/docker-stats.ts +++ b/src/routes/docker-stats.ts @@ -3,8 +3,8 @@ import { Elysia } from "elysia"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; @@ -13,586 +13,586 @@ import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get( - "/containers", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; + .get( + "/containers", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - return responseHandler.error( - set, - pingError as string, - "Docker host connection failed" - ); - } + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + return responseHandler.error( + set, + pingError as string, + "Docker host connection failed", + ); + } - const hostContainers = await docker.listContainers({ all: true }); + const hostContainers = await docker.listContainers({ all: true }); - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - return responseHandler.reject( - set, - reject, - "An error occurred", - error - ); - } - if (!stats) { - return responseHandler.reject( - set, - reject, - "No stats available" - ); - } - resolve(stats); - }); - } - ); + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve, reject) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + return responseHandler.reject( + set, + reject, + "An error occurred", + error, + ); + } + if (!stats) { + return responseHandler.reject( + set, + reject, + "No stats available", + ); + } + resolve(stats); + }); + }, + ); - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError - ); - } - }) - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }) - ); + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats), + memoryUsage: calculateMemoryUsage(stats), + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }), + ); - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", - responses: { - "200": { - description: "Successfully retrieved container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - containers: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "string", - example: "abc123def456", - }, - hostId: { - type: "string", - example: "1", - }, - name: { - type: "string", - example: "example-container", - }, - image: { - type: "string", - example: "nginx:latest", - }, - status: { - type: "string", - example: "running", - }, - state: { - type: "string", - example: "running", - }, - cpuUsage: { - type: "number", - example: 0.5, - }, - memoryUsage: { - type: "number", - example: 1024, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve containers", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", + responses: { + "200": { + description: "Successfully retrieved container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + containers: { + type: "array", + items: { + type: "object", + properties: { + id: { + type: "string", + example: "abc123def456", + }, + hostId: { + type: "string", + example: "1", + }, + name: { + type: "string", + example: "example-container", + }, + image: { + type: "string", + example: "nginx:latest", + }, + status: { + type: "string", + example: "running", + }, + state: { + type: "string", + example: "running", + }, + cpuUsage: { + type: "number", + example: 0.5, + }, + memoryUsage: { + type: "number", + example: 1024, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving container statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve containers", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - logger.debug("Fetched all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts", + async ({ set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const stats: HostStats[] = []; + const stats: HostStats[] = []; - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - stats.push(config); - } + stats.push(config); + } - logger.debug("Fetched stats for all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for all hosts", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/hosts/:id", - async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + logger.debug("Fetched stats for all hosts"); + return stats; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for all hosts", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/hosts/:id", + async ({ params, set }) => { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = findObjectByKey(hosts, "id", Number(params.id)); - if (!host) { - return responseHandler.simple_error( - set, - `Host (${params.id}) not found` - ); - } + const host = findObjectByKey(hosts, "id", Number(params.id)); + if (!host) { + return responseHandler.simple_error( + set, + `Host (${params.id}) not found`, + ); + } - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config" - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ); + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + return responseHandler.error( + set, + error as string, + "Failed to retrieve host config", + ); + } + }, + { + detail: { + tags: ["Statistics"], + description: + "Provides detailed system metrics and Docker runtime information for specified host", + responses: { + "200": { + description: "Successfully retrieved host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + hostId: { + type: "number", + example: 1, + }, + hostName: { + type: "string", + example: "Localhost", + }, + dockerVersion: { + type: "string", + example: "24.0.5", + }, + apiVersion: { + type: "string", + example: "1.41", + }, + os: { + type: "string", + example: "Linux", + }, + architecture: { + type: "string", + example: "x86_64", + }, + totalMemory: { + type: "number", + example: 16777216, + }, + totalCPU: { + type: "number", + example: 4, + }, + labels: { + type: "array", + items: { + type: "string", + }, + example: ["environment=production"], + }, + images: { + type: "number", + example: 10, + }, + containers: { + type: "number", + example: 5, + }, + containersPaused: { + type: "number", + example: 0, + }, + containersRunning: { + type: "number", + example: 4, + }, + containersStopped: { + type: "number", + example: 1, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving host statistics", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Failed to retrieve host config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ); From 84e3f20c43e79fae1db7970ccf71c221c034eb8e Mon Sep 17 00:00:00 2001 From: ItsNik Date: Thu, 19 Jun 2025 04:41:21 +0200 Subject: [PATCH 334/369] ``` feat(config): enhance plugin management and dependency updates This commit introduces enhancements to the plugin management system and updates various dependencies. The plugin management is enhanced by: - Introducing a status indicator for plugins. - Handling errors during plugin registration by setting the plugin status to "inactive". - Displaying both active and inactive plugins in the plugin list. Dependency updates include: - Upgrading `@its_4_nik/gitai` to version 1.1.14. - Upgrading `@types/dockerode` to version 3.3.41. - Upgrading `@types/node` to version 22.15.32. The changes improve the robustness and manageability of plugins and ensure the project is using the latest versions of its dependencies. ``` --- package.json | 6 +- src/core/plugins/loader.ts | 91 +-- src/core/plugins/plugin-manager.ts | 299 ++++---- src/routes/api-config.ts | 1115 ++++++++++++++-------------- src/typings | 2 +- 5 files changed, 771 insertions(+), 742 deletions(-) diff --git a/package.json b/package.json index 4d4f4bb1..0b7a5fb9 100644 --- a/package.json +++ b/package.json @@ -44,11 +44,11 @@ }, "devDependencies": { "@biomejs/biome": "1.9.4", - "@its_4_nik/gitai": "^1.0.10", + "@its_4_nik/gitai": "^1.1.14", "@types/bun": "latest", - "@types/dockerode": "^3.3.40", + "@types/dockerode": "^3.3.41", "@types/js-yaml": "^4.0.9", - "@types/node": "^22.15.31", + "@types/node": "^22.15.32", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts index 854bc5ac..6789cf4c 100644 --- a/src/core/plugins/loader.ts +++ b/src/core/plugins/loader.ts @@ -5,49 +5,50 @@ import { logger } from "../utils/logger"; import { pluginManager } from "./plugin-manager"; export async function loadPlugins(pluginDir: string) { - const pluginPath = path.join(process.cwd(), pluginDir); - - logger.debug(`Loading plugins (${pluginPath})`); - - if (!fs.existsSync(pluginPath)) { - throw new Error("Failed to check plugin directory"); - } - logger.debug("Plugin directory exists"); - - let pluginCount = 0; - let files: string[]; - try { - files = fs.readdirSync(pluginPath); - logger.debug(`Found ${files.length} files in plugin directory`); - } catch (error) { - throw new Error(`Failed to read plugin-directory: ${error}`); - } - - if (!files) { - logger.info(`No plugins found in ${pluginPath}`); - return; - } - - for (const file of files) { - if (!file.endsWith(".plugin.ts")) { - logger.debug(`Skipping non-plugin file: ${file}`); - continue; - } - - const absolutePath = path.join(pluginPath, file); - logger.info(`Loading plugin: ${absolutePath}`); - try { - await checkFileForChangeMe(absolutePath); - const module = await import(absolutePath); - const plugin = module.default; - pluginManager.register(plugin); - pluginCount++; - } catch (error) { - logger.error( - `Error while importing plugin ${absolutePath}: ${error as string}`, - ); - } - } - - logger.info(`Registered ${pluginCount} plugin(s)`); + const pluginPath = path.join(process.cwd(), pluginDir); + + logger.debug(`Loading plugins (${pluginPath})`); + + if (!fs.existsSync(pluginPath)) { + throw new Error("Failed to check plugin directory"); + } + logger.debug("Plugin directory exists"); + + let pluginCount = 0; + let files: string[]; + try { + files = fs.readdirSync(pluginPath); + logger.debug(`Found ${files.length} files in plugin directory`); + } catch (error) { + throw new Error(`Failed to read plugin-directory: ${error}`); + } + + if (!files) { + logger.info(`No plugins found in ${pluginPath}`); + return; + } + + for (const file of files) { + if (!file.endsWith(".plugin.ts")) { + logger.debug(`Skipping non-plugin file: ${file}`); + continue; + } + + const absolutePath = path.join(pluginPath, file); + logger.info(`Loading plugin: ${absolutePath}`); + try { + await checkFileForChangeMe(absolutePath); + const module = await import(absolutePath); + const plugin = module.default; + pluginManager.register(plugin); + pluginCount++; + } catch (error) { + pluginManager.fail({ name: file }); + logger.error( + `Error while registering plugin ${absolutePath}: ${error as string}` + ); + } + } + + logger.info(`Registered ${pluginCount} plugin(s)`); } diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index 2650e8c7..cdf032d0 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -3,143 +3,170 @@ import type { ContainerInfo } from "~/typings/docker"; import type { Hooks, Plugin, PluginInfo } from "~/typings/plugin"; import { logger } from "../utils/logger"; +function getHooks(plugin: Plugin) { + return { + onContainerStart: !!plugin.onContainerStart, + onContainerStop: !!plugin.onContainerStop, + onContainerExit: !!plugin.onContainerExit, + onContainerCreate: !!plugin.onContainerCreate, + onContainerKill: !!plugin.onContainerKill, + handleContainerDie: !!plugin.handleContainerDie, + onContainerDestroy: !!plugin.onContainerDestroy, + onContainerPause: !!plugin.onContainerPause, + onContainerUnpause: !!plugin.onContainerUnpause, + onContainerRestart: !!plugin.onContainerRestart, + onContainerUpdate: !!plugin.onContainerUpdate, + onContainerRename: !!plugin.onContainerRename, + onContainerHealthStatus: !!plugin.onContainerHealthStatus, + onHostUnreachable: !!plugin.onHostUnreachable, + onHostReachableAgain: !!plugin.onHostReachableAgain, + }; +} + class PluginManager extends EventEmitter { - private plugins: Map = new Map(); - - register(plugin: Plugin) { - try { - this.plugins.set(plugin.name, plugin); - logger.debug(`Registered plugin: ${plugin.name}`); - } catch (error) { - logger.error( - `Registering plugin ${plugin.name} failed: ${error as string}`, - ); - } - } - - unregister(name: string) { - this.plugins.delete(name); - } - - getLoadedPlugins(): PluginInfo[] { - return Array.from(this.plugins.values()).map((plugin) => { - const hooks: Hooks = { - onContainerStart: !!plugin.onContainerStart, - onContainerStop: !!plugin.onContainerStop, - onContainerExit: !!plugin.onContainerExit, - onContainerCreate: !!plugin.onContainerCreate, - onContainerKill: !!plugin.onContainerKill, - handleContainerDie: !!plugin.handleContainerDie, - onContainerDestroy: !!plugin.onContainerDestroy, - onContainerPause: !!plugin.onContainerPause, - onContainerUnpause: !!plugin.onContainerUnpause, - onContainerRestart: !!plugin.onContainerRestart, - onContainerUpdate: !!plugin.onContainerUpdate, - onContainerRename: !!plugin.onContainerRename, - onContainerHealthStatus: !!plugin.onContainerHealthStatus, - onHostUnreachable: !!plugin.onHostUnreachable, - onHostReachableAgain: !!plugin.onHostReachableAgain, - }; - - return { - name: plugin.name, - version: plugin.version, - status: "active", - usedHooks: hooks, - }; - }); - } - - // Trigger plugin flows: - handleContainerStop(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerStop?.(containerInfo); - } - } - - handleContainerStart(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerStart?.(containerInfo); - } - } - - handleContainerExit(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerExit?.(containerInfo); - } - } - - handleContainerCreate(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerCreate?.(containerInfo); - } - } - - handleContainerDestroy(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerDestroy?.(containerInfo); - } - } - - handleContainerPause(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerPause?.(containerInfo); - } - } - - handleContainerUnpause(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerUnpause?.(containerInfo); - } - } - - handleContainerRestart(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerRestart?.(containerInfo); - } - } - - handleContainerUpdate(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerUpdate?.(containerInfo); - } - } - - handleContainerRename(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerRename?.(containerInfo); - } - } - - handleContainerHealthStatus(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerHealthStatus?.(containerInfo); - } - } - - handleHostUnreachable(host: string, err: string) { - for (const [, plugin] of this.plugins) { - plugin.onHostUnreachable?.(host, err); - } - } - - handleHostReachableAgain(host: string) { - for (const [, plugin] of this.plugins) { - plugin.onHostReachableAgain?.(host); - } - } - - handleContainerKill(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerKill?.(containerInfo); - } - } - - handleContainerDie(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.handleContainerDie?.(containerInfo); - } - } + private plugins: Map = new Map(); + private failedPlugins: Map = new Map(); + + fail(plugin: Plugin) { + try { + this.failedPlugins.set(plugin.name, plugin); + logger.debug(`Set status to failed for plugin: ${plugin.name}`); + } catch (error) { + logger.error(`Adding failed plugin to list failed: ${error as string}`); + } + } + + register(plugin: Plugin) { + try { + this.plugins.set(plugin.name, plugin); + logger.debug(`Registered plugin: ${plugin.name}`); + } catch (error) { + logger.error( + `Registering plugin ${plugin.name} failed: ${error as string}` + ); + } + } + + unregister(name: string) { + this.plugins.delete(name); + } + + getPlugins(): PluginInfo[] { + const loadedPlugins = Array.from(this.plugins.values()).map((plugin) => { + const hooks: Hooks = getHooks(plugin); + + return { + name: plugin.name, + status: "active", + usedHooks: hooks, + }; + }); + + const failedPlugins = Array.from(this.failedPlugins.values()).map( + (plugin) => { + const hooks: Hooks = getHooks(plugin); + + return { + name: plugin.name, + status: "inactive", + usedHooks: hooks, + }; + } + ); + + return loadedPlugins.concat(failedPlugins); + } + + // Trigger plugin flows: + handleContainerStop(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStop?.(containerInfo); + } + } + + handleContainerStart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStart?.(containerInfo); + } + } + + handleContainerExit(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerExit?.(containerInfo); + } + } + + handleContainerCreate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerCreate?.(containerInfo); + } + } + + handleContainerDestroy(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerDestroy?.(containerInfo); + } + } + + handleContainerPause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerPause?.(containerInfo); + } + } + + handleContainerUnpause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUnpause?.(containerInfo); + } + } + + handleContainerRestart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRestart?.(containerInfo); + } + } + + handleContainerUpdate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUpdate?.(containerInfo); + } + } + + handleContainerRename(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRename?.(containerInfo); + } + } + + handleContainerHealthStatus(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerHealthStatus?.(containerInfo); + } + } + + handleHostUnreachable(host: string, err: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostUnreachable?.(host, err); + } + } + + handleHostReachableAgain(host: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostReachableAgain?.(host); + } + } + + handleContainerKill(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerKill?.(containerInfo); + } + } + + handleContainerDie(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.handleContainerDie?.(containerInfo); + } + } } export const pluginManager = new PluginManager(); diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 8cdec80f..8864a92c 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -5,582 +5,583 @@ import { backupDir } from "~/core/database/backup"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import { responseHandler } from "~/core/utils/response-handler"; import { hashApiKey } from "~/middleware/auth"; import type { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; + .get( + "", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - responses: { - "200": { - description: "Successfully retrieved configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - fetching_interval: { - type: "number", - example: 5, - }, - keep_data_for: { - type: "number", - example: 7, - }, - api_key: { - type: "string", - example: "hashed_api_key", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/plugins", - () => { - try { - return pluginManager.getLoadedPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - responses: { - "200": { - description: "Successfully retrieved plugins", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-plugin", - }, - version: { - type: "string", - example: "1.0.0", - }, - status: { - type: "string", - example: "active", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving plugins", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting all registered plugins", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Returns current API configuration including data retention policies and security settings", + responses: { + "200": { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fetching_interval: { + type: "number", + example: 5, + }, + keep_data_for: { + type: "number", + example: 7, + }, + api_key: { + type: "string", + example: "hashed_api_key", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/plugins", + () => { + try { + return pluginManager.getPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + responses: { + "200": { + description: "Successfully retrieved plugins", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-plugin", + }, + version: { + type: "string", + example: "1.0.0", + }, + status: { + type: "string", + example: "active", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving plugins", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting all registered plugins", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { fetching_interval, keep_data_for, api_key } = body; - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key), - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - responses: { - "200": { - description: "Successfully updated configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated DockStatAPI config", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error updating the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - }, - ) - .get( - "/package", - async () => { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key) + ); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", + responses: { + "200": { + description: "Successfully updated configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated DockStatAPI config", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error updating the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), + } + ) + .get( + "/package", + async () => { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json`, - ); + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json` + ); - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - responses: { - "200": { - description: "Successfully retrieved package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error while reading package.json", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .post( - "/backup", - async ({ set }) => { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return responseHandler.ok(set, backupFilename); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Backs up the internal database", - responses: { - "200": { - description: "Successfully created backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "backup_2024-03-20_12-00-00.db.bak", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error creating backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error backing up", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/backup", - async () => { - try { - const backupFiles = readdirSync(backupDir); + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Displays package metadata including dependencies, contributors, and licensing information", + responses: { + "200": { + description: "Successfully retrieved package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0", + }, + description: { + type: "string", + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", + }, + license: { + type: "string", + example: "CC BY-NC 4.0", + }, + authorName: { + type: "string", + example: "ItsNik", + }, + authorEmail: { + type: "string", + example: "info@itsnik.de", + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik", + }, + contributors: { + type: "array", + items: { + type: "string", + }, + example: [], + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + }, + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error while reading package.json", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .post( + "/backup", + async ({ set }) => { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return responseHandler.ok(set, backupFilename); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Backs up the internal database", + responses: { + "200": { + description: "Successfully created backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "backup_2024-03-20_12-00-00.db.bak", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error creating backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error backing up", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) + .get( + "/backup", + async () => { + try { + const backupFiles = readdirSync(backupDir); - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.startsWith(".") || - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.startsWith(".") || + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Lists all available backups", - responses: { - "200": { - description: "Successfully retrieved backup list", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "string", - }, - example: [ - "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak", - ], - }, - }, - }, - }, - "400": { - description: "Error retrieving backup list", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Reading Backup directory", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Lists all available backups", + responses: { + "200": { + description: "Successfully retrieved backup list", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "string", + }, + example: [ + "backup_2024-03-20_12-00-00.db.bak", + "backup_2024-03-19_12-00-00.db.bak", + ], + }, + }, + }, + }, + "400": { + description: "Error retrieving backup list", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Reading Backup directory", + }, + }, + }, + }, + }, + }, + }, + }, + } + ) - .get( - "/backup/download", - async ({ query, set }) => { - try { - const filename = query.filename || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; + .get( + "/backup/download", + async ({ query, set }) => { + try { + const filename = query.filename || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } - set.headers["Content-Type"] = "application/octet-stream"; - set.headers["Content-Disposition"] = - `attachment; filename="${filename}"`; - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Download a specific backup or the latest if no filename is provided", - responses: { - "200": { - description: "Successfully downloaded backup file", - content: { - "application/octet-stream": { - schema: { - type: "string", - format: "binary", - example: "Binary backup file content", - }, - }, - }, - headers: { - "Content-Disposition": { - schema: { - type: "string", - example: - 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', - }, - }, - }, - }, - "400": { - description: "Error downloading backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Backup download failed", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - filename: t.Optional(t.String()), - }), - }, - ) - .post( - "/restore", - async ({ body, set }) => { - try { - const { file } = body; + set.headers["Content-Type"] = "application/octet-stream"; + set.headers[ + "Content-Disposition" + ] = `attachment; filename="${filename}"`; + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Download a specific backup or the latest if no filename is provided", + responses: { + "200": { + description: "Successfully downloaded backup file", + content: { + "application/octet-stream": { + schema: { + type: "string", + format: "binary", + example: "Binary backup file content", + }, + }, + }, + headers: { + "Content-Disposition": { + schema: { + type: "string", + example: + 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', + }, + }, + }, + }, + "400": { + description: "Error downloading backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Backup download failed", + }, + }, + }, + }, + }, + }, + }, + }, + query: t.Object({ + filename: t.Optional(t.String()), + }), + } + ) + .post( + "/restore", + async ({ body, set }) => { + try { + const { file } = body; - set.headers["Content-Type"] = "text/html"; + set.headers["Content-Type"] = "text/html"; - if (!file) { - throw new Error("No file uploaded"); - } + if (!file) { + throw new Error("No file uploaded"); + } - if (!file.name.endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } + if (!file.name.endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); - return responseHandler.ok(set, "Database restored successfully"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - body: t.Object({ file: t.File() }), - detail: { - tags: ["Management"], - description: "Restore database from uploaded backup file", - responses: { - "200": { - description: "Successfully restored database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Database restored successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restoring database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Database restoration error", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); + return responseHandler.ok(set, "Database restored successfully"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + body: t.Object({ file: t.File() }), + detail: { + tags: ["Management"], + description: "Restore database from uploaded backup file", + responses: { + "200": { + description: "Successfully restored database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Database restored successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error restoring database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Database restoration error", + }, + }, + }, + }, + }, + }, + }, + }, + } + ); diff --git a/src/typings b/src/typings index d0d22fa6..9cae829b 160000 --- a/src/typings +++ b/src/typings @@ -1 +1 @@ -Subproject commit d0d22fa622c5dd9d298d358d4215c8b54cb5f4f3 +Subproject commit 9cae829bead60cd13351b757340f3225649cb11d From 14bc5249f4955dccef4d7197a4bc3a113dfd4b89 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 19 Jun 2025 02:41:45 +0000 Subject: [PATCH 335/369] CQL: Apply lint fixes [skip ci] --- src/core/plugins/loader.ts | 92 +-- src/core/plugins/plugin-manager.ts | 320 ++++---- src/routes/api-config.ts | 1115 ++++++++++++++-------------- 3 files changed, 763 insertions(+), 764 deletions(-) diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts index 6789cf4c..3c058e7c 100644 --- a/src/core/plugins/loader.ts +++ b/src/core/plugins/loader.ts @@ -5,50 +5,50 @@ import { logger } from "../utils/logger"; import { pluginManager } from "./plugin-manager"; export async function loadPlugins(pluginDir: string) { - const pluginPath = path.join(process.cwd(), pluginDir); - - logger.debug(`Loading plugins (${pluginPath})`); - - if (!fs.existsSync(pluginPath)) { - throw new Error("Failed to check plugin directory"); - } - logger.debug("Plugin directory exists"); - - let pluginCount = 0; - let files: string[]; - try { - files = fs.readdirSync(pluginPath); - logger.debug(`Found ${files.length} files in plugin directory`); - } catch (error) { - throw new Error(`Failed to read plugin-directory: ${error}`); - } - - if (!files) { - logger.info(`No plugins found in ${pluginPath}`); - return; - } - - for (const file of files) { - if (!file.endsWith(".plugin.ts")) { - logger.debug(`Skipping non-plugin file: ${file}`); - continue; - } - - const absolutePath = path.join(pluginPath, file); - logger.info(`Loading plugin: ${absolutePath}`); - try { - await checkFileForChangeMe(absolutePath); - const module = await import(absolutePath); - const plugin = module.default; - pluginManager.register(plugin); - pluginCount++; - } catch (error) { - pluginManager.fail({ name: file }); - logger.error( - `Error while registering plugin ${absolutePath}: ${error as string}` - ); - } - } - - logger.info(`Registered ${pluginCount} plugin(s)`); + const pluginPath = path.join(process.cwd(), pluginDir); + + logger.debug(`Loading plugins (${pluginPath})`); + + if (!fs.existsSync(pluginPath)) { + throw new Error("Failed to check plugin directory"); + } + logger.debug("Plugin directory exists"); + + let pluginCount = 0; + let files: string[]; + try { + files = fs.readdirSync(pluginPath); + logger.debug(`Found ${files.length} files in plugin directory`); + } catch (error) { + throw new Error(`Failed to read plugin-directory: ${error}`); + } + + if (!files) { + logger.info(`No plugins found in ${pluginPath}`); + return; + } + + for (const file of files) { + if (!file.endsWith(".plugin.ts")) { + logger.debug(`Skipping non-plugin file: ${file}`); + continue; + } + + const absolutePath = path.join(pluginPath, file); + logger.info(`Loading plugin: ${absolutePath}`); + try { + await checkFileForChangeMe(absolutePath); + const module = await import(absolutePath); + const plugin = module.default; + pluginManager.register(plugin); + pluginCount++; + } catch (error) { + pluginManager.fail({ name: file }); + logger.error( + `Error while registering plugin ${absolutePath}: ${error as string}`, + ); + } + } + + logger.info(`Registered ${pluginCount} plugin(s)`); } diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index cdf032d0..c22c3d59 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -4,169 +4,169 @@ import type { Hooks, Plugin, PluginInfo } from "~/typings/plugin"; import { logger } from "../utils/logger"; function getHooks(plugin: Plugin) { - return { - onContainerStart: !!plugin.onContainerStart, - onContainerStop: !!plugin.onContainerStop, - onContainerExit: !!plugin.onContainerExit, - onContainerCreate: !!plugin.onContainerCreate, - onContainerKill: !!plugin.onContainerKill, - handleContainerDie: !!plugin.handleContainerDie, - onContainerDestroy: !!plugin.onContainerDestroy, - onContainerPause: !!plugin.onContainerPause, - onContainerUnpause: !!plugin.onContainerUnpause, - onContainerRestart: !!plugin.onContainerRestart, - onContainerUpdate: !!plugin.onContainerUpdate, - onContainerRename: !!plugin.onContainerRename, - onContainerHealthStatus: !!plugin.onContainerHealthStatus, - onHostUnreachable: !!plugin.onHostUnreachable, - onHostReachableAgain: !!plugin.onHostReachableAgain, - }; + return { + onContainerStart: !!plugin.onContainerStart, + onContainerStop: !!plugin.onContainerStop, + onContainerExit: !!plugin.onContainerExit, + onContainerCreate: !!plugin.onContainerCreate, + onContainerKill: !!plugin.onContainerKill, + handleContainerDie: !!plugin.handleContainerDie, + onContainerDestroy: !!plugin.onContainerDestroy, + onContainerPause: !!plugin.onContainerPause, + onContainerUnpause: !!plugin.onContainerUnpause, + onContainerRestart: !!plugin.onContainerRestart, + onContainerUpdate: !!plugin.onContainerUpdate, + onContainerRename: !!plugin.onContainerRename, + onContainerHealthStatus: !!plugin.onContainerHealthStatus, + onHostUnreachable: !!plugin.onHostUnreachable, + onHostReachableAgain: !!plugin.onHostReachableAgain, + }; } class PluginManager extends EventEmitter { - private plugins: Map = new Map(); - private failedPlugins: Map = new Map(); - - fail(plugin: Plugin) { - try { - this.failedPlugins.set(plugin.name, plugin); - logger.debug(`Set status to failed for plugin: ${plugin.name}`); - } catch (error) { - logger.error(`Adding failed plugin to list failed: ${error as string}`); - } - } - - register(plugin: Plugin) { - try { - this.plugins.set(plugin.name, plugin); - logger.debug(`Registered plugin: ${plugin.name}`); - } catch (error) { - logger.error( - `Registering plugin ${plugin.name} failed: ${error as string}` - ); - } - } - - unregister(name: string) { - this.plugins.delete(name); - } - - getPlugins(): PluginInfo[] { - const loadedPlugins = Array.from(this.plugins.values()).map((plugin) => { - const hooks: Hooks = getHooks(plugin); - - return { - name: plugin.name, - status: "active", - usedHooks: hooks, - }; - }); - - const failedPlugins = Array.from(this.failedPlugins.values()).map( - (plugin) => { - const hooks: Hooks = getHooks(plugin); - - return { - name: plugin.name, - status: "inactive", - usedHooks: hooks, - }; - } - ); - - return loadedPlugins.concat(failedPlugins); - } - - // Trigger plugin flows: - handleContainerStop(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerStop?.(containerInfo); - } - } - - handleContainerStart(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerStart?.(containerInfo); - } - } - - handleContainerExit(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerExit?.(containerInfo); - } - } - - handleContainerCreate(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerCreate?.(containerInfo); - } - } - - handleContainerDestroy(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerDestroy?.(containerInfo); - } - } - - handleContainerPause(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerPause?.(containerInfo); - } - } - - handleContainerUnpause(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerUnpause?.(containerInfo); - } - } - - handleContainerRestart(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerRestart?.(containerInfo); - } - } - - handleContainerUpdate(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerUpdate?.(containerInfo); - } - } - - handleContainerRename(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerRename?.(containerInfo); - } - } - - handleContainerHealthStatus(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerHealthStatus?.(containerInfo); - } - } - - handleHostUnreachable(host: string, err: string) { - for (const [, plugin] of this.plugins) { - plugin.onHostUnreachable?.(host, err); - } - } - - handleHostReachableAgain(host: string) { - for (const [, plugin] of this.plugins) { - plugin.onHostReachableAgain?.(host); - } - } - - handleContainerKill(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerKill?.(containerInfo); - } - } - - handleContainerDie(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.handleContainerDie?.(containerInfo); - } - } + private plugins: Map = new Map(); + private failedPlugins: Map = new Map(); + + fail(plugin: Plugin) { + try { + this.failedPlugins.set(plugin.name, plugin); + logger.debug(`Set status to failed for plugin: ${plugin.name}`); + } catch (error) { + logger.error(`Adding failed plugin to list failed: ${error as string}`); + } + } + + register(plugin: Plugin) { + try { + this.plugins.set(plugin.name, plugin); + logger.debug(`Registered plugin: ${plugin.name}`); + } catch (error) { + logger.error( + `Registering plugin ${plugin.name} failed: ${error as string}`, + ); + } + } + + unregister(name: string) { + this.plugins.delete(name); + } + + getPlugins(): PluginInfo[] { + const loadedPlugins = Array.from(this.plugins.values()).map((plugin) => { + const hooks: Hooks = getHooks(plugin); + + return { + name: plugin.name, + status: "active", + usedHooks: hooks, + }; + }); + + const failedPlugins = Array.from(this.failedPlugins.values()).map( + (plugin) => { + const hooks: Hooks = getHooks(plugin); + + return { + name: plugin.name, + status: "inactive", + usedHooks: hooks, + }; + }, + ); + + return loadedPlugins.concat(failedPlugins); + } + + // Trigger plugin flows: + handleContainerStop(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStop?.(containerInfo); + } + } + + handleContainerStart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStart?.(containerInfo); + } + } + + handleContainerExit(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerExit?.(containerInfo); + } + } + + handleContainerCreate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerCreate?.(containerInfo); + } + } + + handleContainerDestroy(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerDestroy?.(containerInfo); + } + } + + handleContainerPause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerPause?.(containerInfo); + } + } + + handleContainerUnpause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUnpause?.(containerInfo); + } + } + + handleContainerRestart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRestart?.(containerInfo); + } + } + + handleContainerUpdate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUpdate?.(containerInfo); + } + } + + handleContainerRename(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRename?.(containerInfo); + } + } + + handleContainerHealthStatus(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerHealthStatus?.(containerInfo); + } + } + + handleHostUnreachable(host: string, err: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostUnreachable?.(host, err); + } + } + + handleHostReachableAgain(host: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostReachableAgain?.(host); + } + } + + handleContainerKill(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerKill?.(containerInfo); + } + } + + handleContainerDie(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.handleContainerDie?.(containerInfo); + } + } } export const pluginManager = new PluginManager(); diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts index 8864a92c..c049a044 100644 --- a/src/routes/api-config.ts +++ b/src/routes/api-config.ts @@ -5,583 +5,582 @@ import { backupDir } from "~/core/database/backup"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import { responseHandler } from "~/core/utils/response-handler"; import { hashApiKey } from "~/middleware/auth"; import type { config } from "~/typings/database"; export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; + .get( + "", + async ({ set }) => { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + set.status = 200; - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - responses: { - "200": { - description: "Successfully retrieved configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - fetching_interval: { - type: "number", - example: 5, - }, - keep_data_for: { - type: "number", - example: 7, - }, - api_key: { - type: "string", - example: "hashed_api_key", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/plugins", - () => { - try { - return pluginManager.getPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - responses: { - "200": { - description: "Successfully retrieved plugins", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-plugin", - }, - version: { - type: "string", - example: "1.0.0", - }, - status: { - type: "string", - example: "active", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving plugins", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting all registered plugins", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Returns current API configuration including data retention policies and security settings", + responses: { + "200": { + description: "Successfully retrieved configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fetching_interval: { + type: "number", + example: 5, + }, + keep_data_for: { + type: "number", + example: 7, + }, + api_key: { + type: "string", + example: "hashed_api_key", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/plugins", + () => { + try { + return pluginManager.getPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Lists all active plugins with their registration details and status", + responses: { + "200": { + description: "Successfully retrieved plugins", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string", + example: "example-plugin", + }, + version: { + type: "string", + example: "1.0.0", + }, + status: { + type: "string", + example: "active", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving plugins", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error getting all registered plugins", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .post( + "/update", + async ({ set, body }) => { + try { + const { fetching_interval, keep_data_for, api_key } = body; - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key) - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - responses: { - "200": { - description: "Successfully updated configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated DockStatAPI config", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error updating the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - } - ) - .get( - "/package", - async () => { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key), + ); + return responseHandler.ok(set, "Updated DockStatAPI config"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Modifies core API settings including data collection intervals, retention periods, and security credentials", + responses: { + "200": { + description: "Successfully updated configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Updated DockStatAPI config", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error updating configuration", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error updating the DockStatAPI config", + }, + }, + }, + }, + }, + }, + }, + }, + body: t.Object({ + fetching_interval: t.Number(), + keep_data_for: t.Number(), + api_key: t.String(), + }), + }, + ) + .get( + "/package", + async () => { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json` - ); + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, + ); - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - responses: { - "200": { - description: "Successfully retrieved package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error while reading package.json", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .post( - "/backup", - async ({ set }) => { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return responseHandler.ok(set, backupFilename); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Backs up the internal database", - responses: { - "200": { - description: "Successfully created backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "backup_2024-03-20_12-00-00.db.bak", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error creating backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error backing up", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) - .get( - "/backup", - async () => { - try { - const backupFiles = readdirSync(backupDir); + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Displays package metadata including dependencies, contributors, and licensing information", + responses: { + "200": { + description: "Successfully retrieved package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + version: { + type: "string", + example: "3.0.0", + }, + description: { + type: "string", + example: + "DockStatAPI is an API backend featuring plugins and more for DockStat", + }, + license: { + type: "string", + example: "CC BY-NC 4.0", + }, + authorName: { + type: "string", + example: "ItsNik", + }, + authorEmail: { + type: "string", + example: "info@itsnik.de", + }, + authorWebsite: { + type: "string", + example: "https://github.com/Its4Nik", + }, + contributors: { + type: "array", + items: { + type: "string", + }, + example: [], + }, + dependencies: { + type: "object", + example: { + "@elysiajs/server-timing": "^1.2.1", + "@elysiajs/static": "^1.2.0", + }, + }, + devDependencies: { + type: "object", + example: { + "@biomejs/biome": "1.9.4", + "@types/dockerode": "^3.3.38", + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Error retrieving package information", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error while reading package.json", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .post( + "/backup", + async ({ set }) => { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return responseHandler.ok(set, backupFilename); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Backs up the internal database", + responses: { + "200": { + description: "Successfully created backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "backup_2024-03-20_12-00-00.db.bak", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error creating backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Error backing up", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) + .get( + "/backup", + async () => { + try { + const backupFiles = readdirSync(backupDir); - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.startsWith(".") || - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.startsWith(".") || + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Lists all available backups", - responses: { - "200": { - description: "Successfully retrieved backup list", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "string", - }, - example: [ - "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak", - ], - }, - }, - }, - }, - "400": { - description: "Error retrieving backup list", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Reading Backup directory", - }, - }, - }, - }, - }, - }, - }, - }, - } - ) + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: "Lists all available backups", + responses: { + "200": { + description: "Successfully retrieved backup list", + content: { + "application/json": { + schema: { + type: "array", + items: { + type: "string", + }, + example: [ + "backup_2024-03-20_12-00-00.db.bak", + "backup_2024-03-19_12-00-00.db.bak", + ], + }, + }, + }, + }, + "400": { + description: "Error retrieving backup list", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Reading Backup directory", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ) - .get( - "/backup/download", - async ({ query, set }) => { - try { - const filename = query.filename || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; + .get( + "/backup/download", + async ({ query, set }) => { + try { + const filename = query.filename || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } - set.headers["Content-Type"] = "application/octet-stream"; - set.headers[ - "Content-Disposition" - ] = `attachment; filename="${filename}"`; - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Download a specific backup or the latest if no filename is provided", - responses: { - "200": { - description: "Successfully downloaded backup file", - content: { - "application/octet-stream": { - schema: { - type: "string", - format: "binary", - example: "Binary backup file content", - }, - }, - }, - headers: { - "Content-Disposition": { - schema: { - type: "string", - example: - 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', - }, - }, - }, - }, - "400": { - description: "Error downloading backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Backup download failed", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - filename: t.Optional(t.String()), - }), - } - ) - .post( - "/restore", - async ({ body, set }) => { - try { - const { file } = body; + set.headers["Content-Type"] = "application/octet-stream"; + set.headers["Content-Disposition"] = + `attachment; filename="${filename}"`; + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + detail: { + tags: ["Management"], + description: + "Download a specific backup or the latest if no filename is provided", + responses: { + "200": { + description: "Successfully downloaded backup file", + content: { + "application/octet-stream": { + schema: { + type: "string", + format: "binary", + example: "Binary backup file content", + }, + }, + }, + headers: { + "Content-Disposition": { + schema: { + type: "string", + example: + 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', + }, + }, + }, + }, + "400": { + description: "Error downloading backup", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Backup download failed", + }, + }, + }, + }, + }, + }, + }, + }, + query: t.Object({ + filename: t.Optional(t.String()), + }), + }, + ) + .post( + "/restore", + async ({ body, set }) => { + try { + const { file } = body; - set.headers["Content-Type"] = "text/html"; + set.headers["Content-Type"] = "text/html"; - if (!file) { - throw new Error("No file uploaded"); - } + if (!file) { + throw new Error("No file uploaded"); + } - if (!file.name.endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } + if (!file.name.endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); - return responseHandler.ok(set, "Database restored successfully"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - body: t.Object({ file: t.File() }), - detail: { - tags: ["Management"], - description: "Restore database from uploaded backup file", - responses: { - "200": { - description: "Successfully restored database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Database restored successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restoring database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Database restoration error", - }, - }, - }, - }, - }, - }, - }, - }, - } - ); + return responseHandler.ok(set, "Database restored successfully"); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }, + { + body: t.Object({ file: t.File() }), + detail: { + tags: ["Management"], + description: "Restore database from uploaded backup file", + responses: { + "200": { + description: "Successfully restored database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + example: "Database restored successfully", + }, + }, + }, + }, + }, + }, + "400": { + description: "Error restoring database", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { + type: "string", + example: "Database restoration error", + }, + }, + }, + }, + }, + }, + }, + }, + }, + ); From 956f6624b7ba4d01350367e5014387b5368c3624 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 29 Jun 2025 17:36:23 +0200 Subject: [PATCH 336/369] ``` feat: Refactor routes and handlers This commit introduces a refactoring of the application's route handling and logic by moving route definitions into dedicated handler files. The commit includes the following changes: - Removed the route files - Created handler files - Fixed all the imports/exports This change improves the overall organization and maintainability of the codebase. ``` --- package.json | 8 - src/handlers/config.ts | 193 ++++++++++ src/handlers/database.ts | 13 + src/handlers/docker.ts | 156 ++++++++ src/handlers/index.ts | 13 + src/handlers/logs.ts | 50 +++ src/handlers/stacks.ts | 127 +++++++ src/index.ts | 190 +--------- src/routes/api-config.ts | 586 ------------------------------ src/routes/database-stats.ts | 169 --------- src/routes/docker-manager.ts | 255 ------------- src/routes/docker-stats.ts | 598 ------------------------------- src/routes/logs.ts | 261 -------------- src/routes/stacks.ts | 598 ------------------------------- src/routes/utils.ts | 0 src/tests/api-config.spec.ts | 344 ------------------ src/tests/docker-manager.spec.ts | 482 ------------------------- src/tests/markdown-exporter.ts | 144 -------- src/typings | 2 +- 19 files changed, 555 insertions(+), 3634 deletions(-) create mode 100644 src/handlers/config.ts create mode 100644 src/handlers/database.ts create mode 100644 src/handlers/docker.ts create mode 100644 src/handlers/index.ts create mode 100644 src/handlers/logs.ts create mode 100644 src/handlers/stacks.ts delete mode 100644 src/routes/api-config.ts delete mode 100644 src/routes/database-stats.ts delete mode 100644 src/routes/docker-manager.ts delete mode 100644 src/routes/docker-stats.ts delete mode 100644 src/routes/logs.ts delete mode 100644 src/routes/stacks.ts delete mode 100644 src/routes/utils.ts delete mode 100644 src/tests/api-config.spec.ts delete mode 100644 src/tests/docker-manager.spec.ts delete mode 100644 src/tests/markdown-exporter.ts diff --git a/package.json b/package.json index 0b7a5fb9..0a9529cf 100644 --- a/package.json +++ b/package.json @@ -24,20 +24,12 @@ "lint": "biome check --formatter-enabled=true --linter-enabled=true --organize-imports-enabled=true --fix src" }, "dependencies": { - "@elysiajs/cors": "^1.3.3", - "@elysiajs/html": "^1.3.0", - "@elysiajs/server-timing": "^1.3.0", - "@elysiajs/static": "^1.3.0", - "@elysiajs/swagger": "^1.3.0", "chalk": "^5.4.1", "date-fns": "^4.1.0", "docker-compose": "^1.2.0", "dockerode": "^4.0.7", - "elysia": "latest", - "elysia-remote-dts": "^1.0.3", "js-yaml": "^4.1.0", "knip": "latest", - "logestic": "^1.2.4", "split2": "^4.2.0", "winston": "^3.17.0", "yaml": "^2.8.0" diff --git a/src/handlers/config.ts b/src/handlers/config.ts new file mode 100644 index 00000000..08b0ae16 --- /dev/null +++ b/src/handlers/config.ts @@ -0,0 +1,193 @@ +import { dbFunctions } from "~/core/database"; +import type { config } from "~/typings/database"; +import { logger } from "~/core/utils/logger"; +import { pluginManager } from "~/core/plugins/plugin-manager"; +import { hashApiKey } from "~/middleware/auth"; +import { backupDir } from "~/core/database/backup"; +import { + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, +} from "~/core/utils/package-json"; +import { existsSync, readdirSync, unlinkSync } from "node:fs"; +import { DockerHost } from "~/typings/docker"; + +class apiHandler { + async getConfig() { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateConfig( + fetching_interval: number, + keep_data_for: number, + api_key: string + ) { + try { + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key) + ); + return "Updated DockStatAPI config"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getPlugins() { + try { + return pluginManager.getPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getPackage() { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json` + ); + + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } + + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async createbackup() { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return backupFilename; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async listBackups() { + try { + const backupFiles = readdirSync(backupDir); + + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.startsWith(".") || + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); + + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async downloadbackup(downloadFile?: string) { + try { + const filename: string = downloadFile || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; + + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } + + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async restoreBackup(file: Bun.FileBlob) { + try { + if (!file) { + throw new Error("No file uploaded"); + } + + if (!(file.name || "").endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } + + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return "Database restored successfully"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async addHost(host: DockerHost) { + try { + dbFunctions.addDockerHost(host); + return `Added docker host (${host.name} - ${host.hostAddress})`; + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateHost(host: DockerHost) { + try { + dbFunctions.updateDockerHost(host); + return `Updated docker host (${host.id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async removeHost(id: number) { + try { + dbFunctions.deleteDockerHost(id); + return `Deleted docker host (${id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } +} + +export const ApiHandler = new apiHandler(); diff --git a/src/handlers/database.ts b/src/handlers/database.ts new file mode 100644 index 00000000..6fb95f03 --- /dev/null +++ b/src/handlers/database.ts @@ -0,0 +1,13 @@ +import { dbFunctions } from "~/core/database"; + +class databaseHandler { + async getContainers() { + return dbFunctions.getContainerStats(); + } + + async getHosts() { + return dbFunctions.getHostStats(); + } +} + +export const DatabaseHandler = new databaseHandler(); diff --git a/src/handlers/docker.ts b/src/handlers/docker.ts new file mode 100644 index 00000000..9c7a6251 --- /dev/null +++ b/src/handlers/docker.ts @@ -0,0 +1,156 @@ +import { dbFunctions } from "~/core/database"; +import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; +import { getDockerClient } from "~/core/docker/client"; +import type Docker from "dockerode"; +import { logger } from "~/core/utils/logger"; +import type { DockerInfo } from "~/typings/dockerode"; +import { findObjectByKey } from "~/core/utils/helpers"; + +class basicDockerHandler { + async getContainers() { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; + + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + throw new Error(pingError as string); + } + + const hostContainers = await docker.listContainers({ all: true }); + + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + throw new Error(error as string); + } + if (!stats) { + throw new Error("No stats available"); + } + resolve(stats); + }); + } + ); + + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: stats.cpu_stats.system_cpu_usage, + memoryUsage: stats.memory_stats.usage, + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError + ); + } + }) + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }) + ); + + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getHostStats(id?: number) { + if (!id) { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + const stats: HostStats[] = []; + + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + stats.push(config); + } + + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + throw new Error(error as string); + } + } + + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + const host = findObjectByKey(hosts, "id", Number(id)); + if (!host) { + throw new Error(`Host (${id}) not found`); + } + + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + throw new Error("Failed to retrieve host config"); + } + } +} + +export const BasicDockerHandler = new basicDockerHandler(); diff --git a/src/handlers/index.ts b/src/handlers/index.ts new file mode 100644 index 00000000..aaa64662 --- /dev/null +++ b/src/handlers/index.ts @@ -0,0 +1,13 @@ +import { BasicDockerHandler } from "./docker"; +import { ApiHandler } from "./config"; +import { DatabaseHandler } from "./database"; +import { StackHandler } from "./stacks"; +import { LogHandler } from "./logs"; + +export const handlers = { + BasicDockerHandler, + ApiHandler, + DatabaseHandler, + StackHandler, + LogHandler, +}; diff --git a/src/handlers/logs.ts b/src/handlers/logs.ts new file mode 100644 index 00000000..ffd01852 --- /dev/null +++ b/src/handlers/logs.ts @@ -0,0 +1,50 @@ +import { dbFunctions } from "~/core/database"; +import { logger } from "~/core/utils/logger"; + +class logHandler { + async getLogs(level?: string) { + if (!level) { + try { + const logs = dbFunctions.getAllLogs(); + logger.debug("Retrieved all logs"); + return logs; + } catch (error) { + logger.error("Failed to retrieve logs,", error); + throw new Error("Failed to retrieve logs"); + } + } + try { + const logs = dbFunctions.getLogsByLevel(level); + + logger.debug(`Retrieved logs (level: ${level})`); + return logs; + } catch (error) { + logger.error("Failed to retrieve logs"); + throw new Error(`Failed to retrieve logs`); + } + } + + async deleteLogs(level?: string) { + if (!level) { + try { + dbFunctions.clearAllLogs(); + return { success: true }; + } catch (error) { + logger.error("Could not delete all logs,", error); + throw new Error("Could not delete all logs"); + } + } + + try { + dbFunctions.clearLogsByLevel(level); + + logger.debug(`Cleared all logs with level: ${level}`); + return { success: true }; + } catch (error) { + logger.error("Could not clear logs with level", level, ",", error); + throw new Error("Failed to retrieve logs"); + } + } +} + +export const LogHandler = new logHandler(); diff --git a/src/handlers/stacks.ts b/src/handlers/stacks.ts new file mode 100644 index 00000000..09a8a5db --- /dev/null +++ b/src/handlers/stacks.ts @@ -0,0 +1,127 @@ +import { + deployStack, + getAllStacksStatus, + getStackStatus, + pullStackImages, + removeStack, + restartStack, + startStack, + stopStack, +} from "~/core/stacks/controller"; +import type { stacks_config } from "~/typings/database"; +import { logger } from "~/core/utils/logger"; +import { dbFunctions } from "~/core/database"; + +class stackHandler { + async deploy(config: stacks_config) { + try { + await deployStack(config); + logger.info(`Deployed Stack (${config.name})`); + return `Stack ${config.name} deployed successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error deploying stack, please check the server logs for more information`; + } + } + + async start(stackId: number) { + try { + if (!stackId) { + throw new Error("Stack ID needed"); + } + await startStack(stackId); + logger.info(`Started Stack (${stackId})`); + return `Stack ${stackId} started successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error starting stack`; + } + } + + async stop(stackId: number) { + try { + if (!stackId) { + throw new Error("Stack needed"); + } + await stopStack(stackId); + logger.info(`Stopped Stack (${stackId})`); + return `Stack ${stackId} stopped successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error stopping stack`; + } + } + + async restart(stackId: number) { + try { + if (!stackId) { + throw new Error("StackID needed"); + } + await restartStack(stackId); + logger.info(`Restarted Stack (${stackId})`); + return `Stack ${stackId} restarted successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error restarting stack`; + } + } + + async pullImages(stackId: number) { + try { + if (!stackId) { + throw new Error("StackID needed"); + } + await pullStackImages(stackId); + logger.info(`Pulled Stack images (${stackId})`); + return `Images for stack ${stackId} pulled successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error pulling images`; + } + } + + async getStatus(stackId?: number) { + if (stackId) { + const status = await getStackStatus(stackId); + logger.debug( + `Retrieved status for stackId=${stackId}: ${JSON.stringify(status)}` + ); + return status; + } + + logger.debug("Fetching status for all stacks"); + const status = await getAllStacksStatus(); + logger.debug(`Retrieved status for all stacks: ${JSON.stringify(status)}`); + + return status; + } + + async listStacks() { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks"); + return stacks; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return `${errorMsg}, Error getting stacks`; + } + } + + async deleteStack(stackId: number) { + try { + await removeStack(stackId); + logger.info(`Deleted Stack ${stackId}`); + return `Stack ${stackId} deleted successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return `${errorMsg}, Error deleting stack`; + } + } +} + +export const StackHandler = new stackHandler(); diff --git a/src/index.ts b/src/index.ts index c8f62b60..ade06411 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,189 +1,3 @@ -import cors from "@elysiajs/cors"; -import { serverTiming } from "@elysiajs/server-timing"; -import staticPlugin from "@elysiajs/static"; -import { swagger } from "@elysiajs/swagger"; -import { Elysia } from "elysia"; -import { dts } from "elysia-remote-dts"; -import { Logestic } from "logestic"; -import { dbFunctions } from "~/core/database"; -import { monitorDockerEvents } from "~/core/docker/monitor"; -import { setSchedules } from "~/core/docker/scheduler"; -import { loadPlugins } from "~/core/plugins/loader"; -import { logger } from "~/core/utils/logger"; -import { - authorWebsite, - contributors, - license, -} from "~/core/utils/package-json"; -import { swaggerReadme } from "~/core/utils/swagger-readme"; -import { validateApiKey } from "~/middleware/auth"; -import { apiConfigRoutes } from "~/routes/api-config"; -import { dockerRoutes } from "~/routes/docker-manager"; -import { dockerStatsRoutes } from "~/routes/docker-stats"; -import { dockerWebsocketRoutes } from "~/routes/docker-websocket"; -import { liveLogs } from "~/routes/live-logs"; -import { backendLogs } from "~/routes/logs"; -import { stackRoutes } from "~/routes/stacks"; -import type { config } from "~/typings/database"; -import { checkStacks } from "./core/stacks/checker"; -import { databaseStats } from "./routes/database-stats"; -import { liveStacks } from "./routes/live-stacks"; +import { handlers } from "./handlers"; -console.log(""); - -logger.info("Starting DockStatAPI"); - -const DockStatAPI = new Elysia({ - normalize: true, - precompile: true, -}) - .use(cors()) - //.use(Logestic.preset("fancy")) - .use(staticPlugin()) - .use(serverTiming()) - .use( - dts("./src/index.ts", { - tsconfig: "./tsconfig.json", - compilerOptions: { - strict: true, - }, - }), - ) - .use( - swagger({ - documentation: { - info: { - title: "DockStatAPI", - version: "3.0.0", - description: swaggerReadme, - }, - components: { - securitySchemes: { - apiKeyAuth: { - type: "apiKey" as const, - name: "x-api-key", - in: "header", - description: "API key for authentication", - }, - }, - }, - security: [ - { - apiKeyAuth: [], - }, - ], - tags: [ - { - name: "Statistics", - description: - "All endpoints for fetching statistics of hosts / containers", - }, - { - name: "Management", - description: "Various endpoints for managing DockStatAPI", - }, - { - name: "Stacks", - description: "DockStat's Stack functionality", - }, - { - name: "Utils", - description: "Various utilities which might be useful", - }, - ], - }, - }), - ) - .onBeforeHandle(async (context) => { - const { path, request, set } = context; - - if ( - path === "/health" || - path.startsWith("/swagger") || - path.startsWith("/public") - ) { - logger.info(`Requested unguarded route: ${path}`); - return; - } - - const validation = await validateApiKey(request, set); - - if (!validation) { - throw new Error("Error while checking API key"); - } - - if (!validation.success) { - set.status = 400; - - throw new Error(validation.error); - } - }) - .onError(({ code, set, path, error }) => { - if (code === "NOT_FOUND") { - logger.warn(`Unknown route (${path}), showing error page!`); - set.status = 404; - set.headers["Content-Type"] = "text/html"; - return Bun.file("public/404.html"); - } - - logger.error(`Internal server error at ${path}: ${error}`); - set.status = 500; - set.headers["Content-Type"] = "text/html"; - return { success: false, message: error }; - }) - .use(dockerRoutes) - .use(dockerStatsRoutes) - .use(backendLogs) - .use(dockerWebsocketRoutes) - .use(apiConfigRoutes) - .use(stackRoutes) - .use(liveLogs) - .use(liveStacks) - .use(databaseStats) - .get("/health", () => ({ status: "healthy" }), { - tags: ["Utils"], - response: { message: "healthy" }, - }) - .listen(process.env.DOCKSTATAPI_PORT || 3000, ({ hostname, port }) => { - console.log("----- [ ############## ]"); - logger.info(`DockStatAPI is running at http://${hostname}:${port}`); - logger.info( - `Swagger API Documentation available at http://${hostname}:${port}/swagger`, - ); - logger.info(`License: ${license}`); - logger.info(`Author: ${authorWebsite}`); - logger.info(`Contributors: ${contributors}`); - }); - -const initializeServer = async () => { - try { - await loadPlugins("./src/plugins"); - await setSchedules(); - - monitorDockerEvents().catch((error) => { - logger.error(`Monitoring Error: ${error}`); - }); - - const configData = dbFunctions.getConfig() as config[]; - const apiKey = configData[0].api_key; - - if (apiKey === "changeme") { - logger.warn( - "Default API Key of 'changeme' detected. Please change your API Key via the `/config/update` route!", - ); - } - - await checkStacks(); - - logger.info("Started server"); - console.log("----- [ ############## ]"); - } catch (error) { - logger.error("Error while starting server:", error); - process.exit(1); - } -}; - -await initializeServer(); - -export type App = typeof DockStatAPI; -export { DockStatAPI }; +export default handlers; diff --git a/src/routes/api-config.ts b/src/routes/api-config.ts deleted file mode 100644 index c049a044..00000000 --- a/src/routes/api-config.ts +++ /dev/null @@ -1,586 +0,0 @@ -import { existsSync, readdirSync, unlinkSync } from "node:fs"; -import { Elysia, t } from "elysia"; -import { dbFunctions } from "~/core/database"; -import { backupDir } from "~/core/database/backup"; -import { pluginManager } from "~/core/plugins/plugin-manager"; -import { logger } from "~/core/utils/logger"; -import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, -} from "~/core/utils/package-json"; -import { responseHandler } from "~/core/utils/response-handler"; -import { hashApiKey } from "~/middleware/auth"; -import type { config } from "~/typings/database"; - -export const apiConfigRoutes = new Elysia({ prefix: "/config" }) - .get( - "", - async ({ set }) => { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - set.status = 200; - - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Returns current API configuration including data retention policies and security settings", - responses: { - "200": { - description: "Successfully retrieved configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - fetching_interval: { - type: "number", - example: 5, - }, - keep_data_for: { - type: "number", - example: 7, - }, - api_key: { - type: "string", - example: "hashed_api_key", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/plugins", - () => { - try { - return pluginManager.getPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all active plugins with their registration details and status", - responses: { - "200": { - description: "Successfully retrieved plugins", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-plugin", - }, - version: { - type: "string", - example: "1.0.0", - }, - status: { - type: "string", - example: "active", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving plugins", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting all registered plugins", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .post( - "/update", - async ({ set, body }) => { - try { - const { fetching_interval, keep_data_for, api_key } = body; - - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key), - ); - return responseHandler.ok(set, "Updated DockStatAPI config"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies core API settings including data collection intervals, retention periods, and security credentials", - responses: { - "200": { - description: "Successfully updated configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated DockStatAPI config", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating configuration", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error updating the DockStatAPI config", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - fetching_interval: t.Number(), - keep_data_for: t.Number(), - api_key: t.String(), - }), - }, - ) - .get( - "/package", - async () => { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json`, - ); - - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } - - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Displays package metadata including dependencies, contributors, and licensing information", - responses: { - "200": { - description: "Successfully retrieved package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - version: { - type: "string", - example: "3.0.0", - }, - description: { - type: "string", - example: - "DockStatAPI is an API backend featuring plugins and more for DockStat", - }, - license: { - type: "string", - example: "CC BY-NC 4.0", - }, - authorName: { - type: "string", - example: "ItsNik", - }, - authorEmail: { - type: "string", - example: "info@itsnik.de", - }, - authorWebsite: { - type: "string", - example: "https://github.com/Its4Nik", - }, - contributors: { - type: "array", - items: { - type: "string", - }, - example: [], - }, - dependencies: { - type: "object", - example: { - "@elysiajs/server-timing": "^1.2.1", - "@elysiajs/static": "^1.2.0", - }, - }, - devDependencies: { - type: "object", - example: { - "@biomejs/biome": "1.9.4", - "@types/dockerode": "^3.3.38", - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving package information", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error while reading package.json", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .post( - "/backup", - async ({ set }) => { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return responseHandler.ok(set, backupFilename); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Backs up the internal database", - responses: { - "200": { - description: "Successfully created backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "backup_2024-03-20_12-00-00.db.bak", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error creating backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error backing up", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/backup", - async () => { - try { - const backupFiles = readdirSync(backupDir); - - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.startsWith(".") || - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); - - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: "Lists all available backups", - responses: { - "200": { - description: "Successfully retrieved backup list", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "string", - }, - example: [ - "backup_2024-03-20_12-00-00.db.bak", - "backup_2024-03-19_12-00-00.db.bak", - ], - }, - }, - }, - }, - "400": { - description: "Error retrieving backup list", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Reading Backup directory", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - - .get( - "/backup/download", - async ({ query, set }) => { - try { - const filename = query.filename || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; - - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } - - set.headers["Content-Type"] = "application/octet-stream"; - set.headers["Content-Disposition"] = - `attachment; filename="${filename}"`; - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Download a specific backup or the latest if no filename is provided", - responses: { - "200": { - description: "Successfully downloaded backup file", - content: { - "application/octet-stream": { - schema: { - type: "string", - format: "binary", - example: "Binary backup file content", - }, - }, - }, - headers: { - "Content-Disposition": { - schema: { - type: "string", - example: - 'attachment; filename="backup_2024-03-20_12-00-00.db.bak"', - }, - }, - }, - }, - "400": { - description: "Error downloading backup", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Backup download failed", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - filename: t.Optional(t.String()), - }), - }, - ) - .post( - "/restore", - async ({ body, set }) => { - try { - const { file } = body; - - set.headers["Content-Type"] = "text/html"; - - if (!file) { - throw new Error("No file uploaded"); - } - - if (!file.name.endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } - - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); - - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); - - return responseHandler.ok(set, "Database restored successfully"); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - body: t.Object({ file: t.File() }), - detail: { - tags: ["Management"], - description: "Restore database from uploaded backup file", - responses: { - "200": { - description: "Successfully restored database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Database restored successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restoring database", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Database restoration error", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); diff --git a/src/routes/database-stats.ts b/src/routes/database-stats.ts deleted file mode 100644 index 8a1bd882..00000000 --- a/src/routes/database-stats.ts +++ /dev/null @@ -1,169 +0,0 @@ -import Elysia from "elysia"; -import { dbFunctions } from "~/core/database"; - -export const databaseStats = new Elysia({ prefix: "/db-stats" }) - .get( - "/containers", - async () => { - return dbFunctions.getContainerStats(); - }, - { - detail: { - tags: ["Statistics"], - description: "Shows all stored metrics of containers", - responses: { - "200": { - description: "Successfully fetched Container Stats from the DB", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "string", - example: - "0c1142d825a4104f45099e8297428cc7ef820319924aa9cf46739cf1c147cdae", - }, - hostId: { - type: "string", - example: "Localhost", - }, - name: { - type: "string", - example: "heimdall", - }, - image: { - type: "string", - example: "linuxserver/heimdall:latest", - }, - status: { - type: "string", - example: "Up About a minute", - }, - state: { - type: "string", - example: "running", - }, - cpu_usage: { - type: "number", - example: 0.00628140703517588, - }, - memory_usage: { - type: "number", - example: 0.2784590652462969, - }, - timestamp: { - type: "string", - example: "2025-06-07 07:01:26", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/hosts", - async () => { - return dbFunctions.getHostStats(); - }, - { - detail: { - tags: ["Statistics"], - description: "Shows all stored metrics of Docker hosts", - responses: { - "200": { - description: "Successfully fetched Host Stats from the DB", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - description: "Unique identifier for the host", - }, - hostName: { - type: "string", - example: "Localhost", - description: "Display name of the host", - }, - dockerVersion: { - type: "string", - example: "28.2.0", - description: "Installed Docker version", - }, - apiVersion: { - type: "string", - example: "overlay2", - description: "Docker API version", - }, - os: { - type: "string", - example: "Arch Linux", - description: "Host operating system", - }, - architecture: { - type: "string", - example: "x86_64", - description: "System architecture", - }, - totalMemory: { - type: "number", - example: 33512706048, - description: "Total system memory in bytes", - }, - totalCPU: { - type: "number", - example: 4, - description: "Number of available CPU cores", - }, - labels: { - type: "string", - example: "[]", - description: "JSON string of host labels", - }, - containers: { - type: "number", - example: 3, - description: "Total containers on host", - }, - containersRunning: { - type: "number", - example: 3, - description: "Currently running containers", - }, - containersStopped: { - type: "number", - example: 0, - description: "Stopped containers", - }, - containersPaused: { - type: "number", - example: 0, - description: "Paused containers", - }, - images: { - type: "number", - example: 30, - description: "Available Docker images", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); diff --git a/src/routes/docker-manager.ts b/src/routes/docker-manager.ts deleted file mode 100644 index fcd877e9..00000000 --- a/src/routes/docker-manager.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { Elysia, t } from "elysia"; -import { dbFunctions } from "~/core/database"; -import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/response-handler"; -import type { DockerHost } from "~/typings/docker"; - -export const dockerRoutes = new Elysia({ prefix: "/docker-config" }) - .post( - "/add-host", - async ({ set, body }) => { - try { - dbFunctions.addDockerHost(body as DockerHost); - return responseHandler.ok(set, `Added docker host (${body.name})`); - } catch (error: unknown) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Registers a new Docker host to the monitoring system with connection details", - responses: { - "200": { - description: "Successfully added Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Added docker host (Localhost)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error adding Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error adding docker Host", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - name: t.String(), - hostAddress: t.String(), - secure: t.Boolean(), - }), - }, - ) - - .post( - "/update-host", - async ({ set, body }) => { - try { - set.status = 200; - dbFunctions.updateDockerHost(body); - return responseHandler.ok(set, `Updated docker host (${body.id})`); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Modifies existing Docker host configuration parameters (name, address, security)", - responses: { - "200": { - description: "Successfully updated Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Updated docker host (1)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error updating Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to update host", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - id: t.Number(), - name: t.String(), - hostAddress: t.String(), - secure: t.Boolean(), - }), - }, - ) - - .get( - "/hosts", - async ({ set }) => { - try { - const dockerHosts = dbFunctions.getDockerHosts(); - - logger.debug("Retrieved docker hosts"); - return dockerHosts; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Lists all configured Docker hosts with their connection settings", - responses: { - "200": { - description: "Successfully retrieved Docker hosts", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "number", - example: 1, - }, - name: { - type: "string", - example: "Localhost", - }, - hostAddress: { - type: "string", - example: "localhost:2375", - }, - secure: { - type: "boolean", - example: false, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving Docker hosts", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve hosts", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - - .delete( - "/hosts/:id", - async ({ set, params }) => { - try { - set.status = 200; - dbFunctions.deleteDockerHost(params.id); - return responseHandler.ok(set, `Deleted docker host (${params.id})`); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Management"], - description: - "Removes Docker host from monitoring system and clears associated data", - responses: { - "200": { - description: "Successfully deleted Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Deleted docker host (1)", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error deleting Docker host", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to delete host", - }, - }, - }, - }, - }, - }, - }, - }, - params: t.Object({ - id: t.Number(), - }), - }, - ); diff --git a/src/routes/docker-stats.ts b/src/routes/docker-stats.ts deleted file mode 100644 index aa968d2d..00000000 --- a/src/routes/docker-stats.ts +++ /dev/null @@ -1,598 +0,0 @@ -import type Docker from "dockerode"; -import { Elysia } from "elysia"; -import { dbFunctions } from "~/core/database"; -import { getDockerClient } from "~/core/docker/client"; -import { - calculateCpuPercent, - calculateMemoryUsage, -} from "~/core/utils/calculations"; -import { findObjectByKey } from "~/core/utils/helpers"; -import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/response-handler"; -import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; -import type { DockerInfo } from "~/typings/dockerode"; - -export const dockerStatsRoutes = new Elysia({ prefix: "/docker" }) - .get( - "/containers", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; - - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - return responseHandler.error( - set, - pingError as string, - "Docker host connection failed", - ); - } - - const hostContainers = await docker.listContainers({ all: true }); - - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - return responseHandler.reject( - set, - reject, - "An error occurred", - error, - ); - } - if (!stats) { - return responseHandler.reject( - set, - reject, - "No stats available", - ); - } - resolve(stats); - }); - }, - ); - - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats), - memoryUsage: calculateMemoryUsage(stats), - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError, - ); - } - }), - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }), - ); - - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Collects real-time statistics for all Docker containers across monitored hosts, including CPU and memory utilization", - responses: { - "200": { - description: "Successfully retrieved container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - containers: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "string", - example: "abc123def456", - }, - hostId: { - type: "string", - example: "1", - }, - name: { - type: "string", - example: "example-container", - }, - image: { - type: "string", - example: "nginx:latest", - }, - status: { - type: "string", - example: "running", - }, - state: { - type: "string", - example: "running", - }, - cpuUsage: { - type: "number", - example: 0.5, - }, - memoryUsage: { - type: "number", - example: 1024, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving container statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve containers", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - - const stats: HostStats[] = []; - - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - stats.push(config); - } - - logger.debug("Fetched all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/hosts", - async ({ set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - - const stats: HostStats[] = []; - - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - stats.push(config); - } - - logger.debug("Fetched stats for all hosts"); - return stats; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for all hosts", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .get( - "/hosts/:id", - async ({ params, set }) => { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - - const host = findObjectByKey(hosts, "id", Number(params.id)); - if (!host) { - return responseHandler.simple_error( - set, - `Host (${params.id}) not found`, - ); - } - - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - return responseHandler.error( - set, - error as string, - "Failed to retrieve host config", - ); - } - }, - { - detail: { - tags: ["Statistics"], - description: - "Provides detailed system metrics and Docker runtime information for specified host", - responses: { - "200": { - description: "Successfully retrieved host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - hostId: { - type: "number", - example: 1, - }, - hostName: { - type: "string", - example: "Localhost", - }, - dockerVersion: { - type: "string", - example: "24.0.5", - }, - apiVersion: { - type: "string", - example: "1.41", - }, - os: { - type: "string", - example: "Linux", - }, - architecture: { - type: "string", - example: "x86_64", - }, - totalMemory: { - type: "number", - example: 16777216, - }, - totalCPU: { - type: "number", - example: 4, - }, - labels: { - type: "array", - items: { - type: "string", - }, - example: ["environment=production"], - }, - images: { - type: "number", - example: 10, - }, - containers: { - type: "number", - example: 5, - }, - containersPaused: { - type: "number", - example: 0, - }, - containersRunning: { - type: "number", - example: 4, - }, - containersStopped: { - type: "number", - example: 1, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error retrieving host statistics", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve host config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); diff --git a/src/routes/logs.ts b/src/routes/logs.ts deleted file mode 100644 index 17da1fb7..00000000 --- a/src/routes/logs.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { Elysia } from "elysia"; - -import { dbFunctions } from "~/core/database"; -import { logger } from "~/core/utils/logger"; - -export const backendLogs = new Elysia({ prefix: "/logs" }) - .get( - "", - async ({ set }) => { - try { - const logs = dbFunctions.getAllLogs(); - // - logger.debug("Retrieved all logs"); - return logs; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve logs,", error); - return { error: "Failed to retrieve logs" }; - } - }, - { - detail: { - tags: ["Management"], - description: - "Retrieves complete application log history from persistent storage", - responses: { - "200": { - description: "Successfully retrieved logs", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "number", - example: 1, - }, - level: { - type: "string", - example: "info", - }, - message: { - type: "string", - example: "Application started", - }, - timestamp: { - type: "string", - example: "2024-03-20T12:00:00Z", - }, - }, - }, - }, - }, - }, - }, - "500": { - description: "Error retrieving logs", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve logs", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - - .get( - "/:level", - async ({ params: { level }, set }) => { - try { - const logs = dbFunctions.getLogsByLevel(level); - - logger.debug(`Retrieved logs (level: ${level})`); - return logs; - } catch (error) { - set.status = 500; - logger.error("Failed to retrieve logs"); - return { error: "Failed to retrieve logs" }; - } - }, - { - detail: { - tags: ["Management"], - description: - "Filters logs by severity level (debug, info, warn, error, fatal)", - responses: { - "200": { - description: "Successfully retrieved logs by level", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "number", - example: 1, - }, - level: { - type: "string", - example: "info", - }, - message: { - type: "string", - example: "Application started", - }, - timestamp: { - type: "string", - example: "2024-03-20T12:00:00Z", - }, - }, - }, - }, - }, - }, - }, - "500": { - description: "Error retrieving logs", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve logs", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - - .delete( - "/", - async ({ set }) => { - try { - set.status = 200; - - dbFunctions.clearAllLogs(); - return { success: true }; - } catch (error) { - set.status = 500; - logger.error("Could not delete all logs,", error); - return { error: "Could not delete all logs" }; - } - }, - { - detail: { - tags: ["Management"], - description: "Purges all historical log records from the database", - responses: { - "200": { - description: "Successfully cleared all logs", - content: { - "application/json": { - schema: { - type: "object", - properties: { - success: { - type: "boolean", - example: true, - }, - }, - }, - }, - }, - }, - "500": { - description: "Error clearing logs", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Could not delete all logs", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - - .delete( - "/:level", - async ({ params: { level }, set }) => { - try { - dbFunctions.clearLogsByLevel(level); - - logger.debug(`Cleared all logs with level: ${level}`); - return { success: true }; - } catch (error) { - set.status = 500; - logger.error("Could not clear logs with level", level, ",", error); - return { error: "Failed to retrieve logs" }; - } - }, - { - detail: { - tags: ["Management"], - description: "Clears log entries matching specified severity level", - responses: { - "200": { - description: "Successfully cleared logs by level", - content: { - "application/json": { - schema: { - type: "object", - properties: { - success: { - type: "boolean", - example: true, - }, - }, - }, - }, - }, - }, - "500": { - description: "Error clearing logs", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Failed to retrieve logs", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ); diff --git a/src/routes/stacks.ts b/src/routes/stacks.ts deleted file mode 100644 index d81d24d6..00000000 --- a/src/routes/stacks.ts +++ /dev/null @@ -1,598 +0,0 @@ -import { Elysia, t } from "elysia"; -import { dbFunctions } from "~/core/database"; -import { - deployStack, - getAllStacksStatus, - getStackStatus, - pullStackImages, - removeStack, - restartStack, - startStack, - stopStack, -} from "~/core/stacks/controller"; -import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/response-handler"; -import type { stacks_config } from "~/typings/database"; - -export const stackRoutes = new Elysia({ prefix: "/stacks" }) - .post( - "/deploy", - async ({ set, body }) => { - try { - await deployStack(body as stacks_config); - logger.info(`Deployed Stack (${body.name})`); - return responseHandler.ok( - set, - `Stack ${body.name} deployed successfully`, - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return responseHandler.error( - set, - errorMsg, - "Error deploying stack, please check the server logs for more information", - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Deploys a new Docker stack using a provided compose specification, allowing custom configurations and image updates", - responses: { - "200": { - description: "Successfully deployed stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack example-stack deployed successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error deploying stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error deploying stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - name: t.String(), - version: t.Number(), - custom: t.Boolean(), - source: t.String(), - compose_spec: t.Any(), - }), - }, - ) - .post( - "/start", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack ID needed"); - } - await startStack(body.stackId); - logger.info(`Started Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} started successfully`, - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return responseHandler.error(set, errorMsg, "Error starting stack"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Initiates a Docker stack, starting all associated containers", - responses: { - "200": { - description: "Successfully started stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 started successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error starting stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error starting stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - }, - ) - .post( - "/stop", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await stopStack(body.stackId); - logger.info(`Stopped Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} stopped successfully`, - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return responseHandler.error(set, errorMsg, "Error stopping stack"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Halts a running Docker stack and its containers while preserving configurations", - responses: { - "200": { - description: "Successfully stopped stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 stopped successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error stopping stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error stopping stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - }, - ) - .post( - "/restart", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await restartStack(body.stackId); - logger.info(`Restarted Stack (${body.stackId})`); - return responseHandler.ok( - set, - `Stack ${body.stackId} restarted successfully`, - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return responseHandler.error(set, errorMsg, "Error restarting stack"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Performs full stack restart - stops and restarts all stack components in sequence", - responses: { - "200": { - description: "Successfully restarted stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 restarted successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error restarting stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error restarting stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - }, - ) - .post( - "/pull-images", - async ({ set, body }) => { - try { - if (!body.stackId) { - throw new Error("Stack needed"); - } - await pullStackImages(body.stackId); - logger.info(`Pulled Stack images (${body.stackId})`); - return responseHandler.ok( - set, - `Images for stack ${body.stackId} pulled successfully`, - ); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return responseHandler.error(set, errorMsg, "Error pulling images"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Updates container images for a stack using Docker's pull mechanism (requires stack ID)", - responses: { - "200": { - description: "Successfully pulled images", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Images for stack 1 pulled successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error pulling images", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error pulling images", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - }, - ) - .get( - "/status", - async ({ set, query }) => { - try { - // biome-ignore lint/suspicious/noExplicitAny: - let status: Record; - let res = {}; - - logger.debug("Entering stack status handler"); - logger.debug(`Request body: ${JSON.stringify(query)}`); - - if (query.stackId !== 0) { - logger.debug(`Fetching status for stackId=${query.stackId}`); - status = await getStackStatus(query.stackId); - logger.debug( - `Retrieved status for stackId=${query.stackId}: ${JSON.stringify( - status, - )}`, - ); - - res = responseHandler.ok( - set, - `Stack ${query.stackId} status retrieved successfully`, - ); - logger.info("Fetched Stack status"); - } else { - logger.debug("Fetching status for all stacks"); - status = await getAllStacksStatus(); - logger.debug( - `Retrieved status for all stacks: ${JSON.stringify(status)}`, - ); - - res = responseHandler.ok(set, "Fetched all Stack's status"); - logger.info("Fetched all Stack status"); - } - - logger.debug("Returning response with status data"); - return { ...res, status: status }; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - logger.debug(`Error occurred while fetching stack status: ${errorMsg}`); - - return responseHandler.error( - set, - errorMsg, - "Error getting stack status", - ); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Retrieves operational status for either a specific stack (by ID) or all managed stacks (ID: 0)", - responses: { - "200": { - description: "Successfully retrieved stack status", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 status retrieved successfully", - }, - status: { - type: "object", - properties: { - name: { - type: "string", - example: "example-stack", - }, - status: { - type: "string", - example: "running", - }, - containers: { - type: "array", - items: { - type: "object", - properties: { - name: { - type: "string", - example: "example-stack_web_1", - }, - status: { - type: "string", - example: "running", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error getting stack status", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting stack status", - }, - }, - }, - }, - }, - }, - }, - }, - query: t.Object({ - stackId: t.Number(), - }), - }, - ) - .get( - "/", - async ({ set }) => { - try { - const stacks = dbFunctions.getStacks(); - logger.info("Fetched Stacks"); - return stacks; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return responseHandler.error(set, errorMsg, "Error getting stacks"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Lists all registered stacks with their complete configuration details", - responses: { - "200": { - description: "Successfully retrieved stacks", - content: { - "application/json": { - schema: { - type: "array", - items: { - type: "object", - properties: { - id: { - type: "number", - example: 1, - }, - name: { - type: "string", - example: "example-stack", - }, - version: { - type: "number", - example: 1, - }, - source: { - type: "string", - example: "github.com/example/repo", - }, - automatic_reboot_on_error: { - type: "boolean", - example: true, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Error getting stacks", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error getting stacks", - }, - }, - }, - }, - }, - }, - }, - }, - }, - ) - .delete( - "/", - async ({ set, body }) => { - try { - const { stackId } = body; - await removeStack(stackId); - logger.info(`Deleted Stack ${stackId}`); - return responseHandler.ok(set, `Stack ${stackId} deleted successfully`); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return responseHandler.error(set, errorMsg, "Error deleting stack"); - } - }, - { - detail: { - tags: ["Stacks"], - description: - "Permanently removes a stack configuration and cleans up associated resources", - responses: { - "200": { - description: "Successfully deleted stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { - type: "string", - example: "Stack 1 deleted successfully", - }, - }, - }, - }, - }, - }, - "400": { - description: "Error deleting stack", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { - type: "string", - example: "Error deleting stack", - }, - }, - }, - }, - }, - }, - }, - }, - body: t.Object({ - stackId: t.Number(), - }), - }, - ); diff --git a/src/routes/utils.ts b/src/routes/utils.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tests/api-config.spec.ts b/src/tests/api-config.spec.ts deleted file mode 100644 index ba3e7b32..00000000 --- a/src/tests/api-config.spec.ts +++ /dev/null @@ -1,344 +0,0 @@ -import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; -import { Elysia } from "elysia"; -import { logger } from "~/core/utils/logger"; -import { apiConfigRoutes } from "~/routes/api-config"; -import { generateMarkdownReport, recordTestResult } from "./markdown-exporter"; -import type { TestContext } from "./markdown-exporter"; - -const mockDb = { - updateConfig: mock(() => ({})), - backupDatabase: mock( - () => `dockstatapi-${new Date().toISOString().slice(0, 10)}.db.bak`, - ), - restoreDatabase: mock(), - findLatestBackup: mock(() => "dockstatapi-2025-05-06.db.bak"), -}; - -mock.module("node:fs", () => ({ - existsSync: mock((path) => path.includes("dockstatapi")), - readdirSync: mock(() => [ - "dockstatapi-2025-05-06.db.bak", - "dockstatapi.db", - "dockstatapi.db-shm", - ]), - unlinkSync: mock(), -})); - -const mockPlugins = [ - { - name: "docker-monitor", - version: "1.2.0", - status: "active", - }, -]; - -const createTestApp = () => - new Elysia().use(apiConfigRoutes).decorate({ - dbFunctions: mockDb, - pluginManager: { - getLoadedPlugins: mock(() => mockPlugins), - getPlugin: mock((name) => mockPlugins.find((p) => p.name === name)), - }, - logger: { - ...logger, - debug: mock(), - error: mock(), - info: mock(), - }, - }); - -async function captureTestContext( - req: Request, - res: Response, -): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: string; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch (textError) { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; -} - -describe("API Configuration Endpoints", () => { - beforeEach(() => { - mockDb.updateConfig.mockClear(); - }); - - describe("Core Configuration", () => { - it("should retrieve current config with hashed API key", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - fetching_interval: expect.any(Number), - keep_data_for: expect.any(Number), - }); - - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should retrieve current config with hashed API key", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with valid config structure", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle config update with valid payload", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const requestBody = { - fetching_interval: 15, - keep_data_for: 30, - api_key: "new-valid-key", - }; - const req = new Request("http://localhost:3000/config/update", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(requestBody), - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - success: true, - message: expect.stringContaining("Updated"), - }); - - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should handle config update with valid payload", - suite: "API Configuration Endpoints - Core Configuration", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Plugin Management", () => { - it("should list active plugins with metadata", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/plugins"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual( - [], - //expect.arrayContaining([ - // expect.objectContaining({ - // name: expect.any(String), - // version: expect.any(String), - // status: expect.any(String), - // }), - //]) - ); - - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should list active plugins with metadata", - suite: "API Configuration Endpoints - Plugin Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with plugin list", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Backup Management", () => { - it("should generate timestamped backup files", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/backup", { - method: "POST", - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - const { message } = context.response.body as { message: string }; - expect(message).toMatch( - /^data\/dockstatapi-\d{2}-\d{2}-\d{4}-1\.db\.bak$/, - ); - - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should generate timestamped backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with backup path", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should list valid backup files", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/config/backup"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - const backups = context.response.body as string[]; - expect(backups).toEqual( - expect.arrayContaining([expect.stringMatching(/\.db\.bak$/)]), - ); - - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should list valid backup files", - suite: "API Configuration Endpoints - Backup Management", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with backup list", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("Error Handling", () => { - it("should return proper error format", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - try { - const app = createTestApp(); - const req = new Request("http://localhost:3000/random_link", { - method: "GET", - headers: { "Content-Type": "application/json" }, - }); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(404); - - recordTestResult({ - name: "should return proper error format", - suite: - "API Configuration Endpoints - Error Handling of unkown routes", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "should return proper error format", - suite: "API Configuration Endpoints - Error Handling", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "500 Error with structured error format", - received: context?.response, - }, - }); - throw error; - } - }); - }); -}); - -afterAll(() => { - generateMarkdownReport(); -}); diff --git a/src/tests/docker-manager.spec.ts b/src/tests/docker-manager.spec.ts deleted file mode 100644 index 865b2aa1..00000000 --- a/src/tests/docker-manager.spec.ts +++ /dev/null @@ -1,482 +0,0 @@ -import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"; -import { Elysia } from "elysia"; -import { dbFunctions } from "~/core/database"; -import { dockerRoutes } from "~/routes/docker-manager"; -import { - generateMarkdownReport, - recordTestResult, - testResults, -} from "./markdown-exporter"; -import type { TestContext } from "./markdown-exporter"; - -type DockerHost = { - id?: number; - name: string; - hostAddress: string; - secure: boolean; -}; - -const mockDb = { - addDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - updateDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), - getDockerHosts: mock(() => []), - deleteDockerHost: mock(() => ({ - changes: 1, - lastInsertRowid: 1, - })), -}; - -mock.module("~/core/database", () => ({ - dbFunctions: mockDb, -})); - -mock.module("~/core/utils/logger", () => ({ - logger: { - debug: mock(), - info: mock(), - error: mock(), - }, -})); - -const createApp = () => new Elysia().use(dockerRoutes).decorate({}); - -async function captureTestContext( - req: Request, - res: Response, -): Promise { - const responseStatus = res.status; - const responseHeaders = Object.fromEntries(res.headers.entries()); - let responseBody: unknown; - - try { - responseBody = await res.clone().json(); - } catch (parseError) { - try { - responseBody = await res.clone().text(); - } catch { - responseBody = "Unparseable response content"; - } - } - - return { - request: { - method: req.method, - url: req.url, - headers: Object.fromEntries(req.headers.entries()), - body: req.body ? await req.clone().text() : undefined, - }, - response: { - status: responseStatus, - headers: responseHeaders, - body: responseBody, - }, - }; -} - -describe("Docker Configuration Endpoints", () => { - beforeEach(() => { - mockDb.addDockerHost.mockClear(); - mockDb.updateDockerHost.mockClear(); - mockDb.getDockerHosts.mockClear(); - mockDb.deleteDockerHost.mockClear(); - }); - - describe("POST /docker-config/add-host", () => { - it("should add a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host1", - hostAddress: "127.0.0.1:2375", - secure: false, - }; - - try { - const app = createApp(); - const req = new Request( - "http://localhost:3000/docker-config/add-host", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }, - ); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Added docker host (${host.name})`, - }); - expect(mockDb.addDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host success", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with success message", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when adding a docker host fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - name: "Host2", - hostAddress: "invalid", - secure: true, - }; - - // Set mock implementation - mockDb.addDockerHost.mockImplementationOnce(() => { - throw new Error("Mock Database Error"); - }); - - try { - const app = createApp(); - const req = new Request( - "http://localhost:3000/docker-config/add-host", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }, - ); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response).toMatchObject({ - body: expect.any(String), - }); - - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "add-host failure", - suite: "Docker Config - Add Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error structure", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("POST /docker-config/update-host", () => { - it("should update a docker host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 1, - name: "Host1-upd", - hostAddress: "127.0.0.1:2376", - secure: true, - }; - - try { - const app = createApp(); - const req = new Request( - "http://localhost:3000/docker-config/update-host", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }, - ); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Updated docker host (${host.id})`, - }); - expect(mockDb.updateDockerHost).toHaveBeenCalledWith(host); - - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host success", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with update confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when update fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const host: DockerHost = { - id: 2, - name: "Host2", - hostAddress: "x", - secure: false, - }; - - mockDb.updateDockerHost.mockImplementationOnce(() => { - throw new Error("Update error"); - }); - - try { - const app = createApp(); - const req = new Request( - "http://localhost:3000/docker-config/update-host", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(host), - }, - ); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response).toMatchObject({ - body: expect.any(String), - }); - - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "update-host failure", - suite: "Docker Config - Update Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("GET /docker-config/hosts", () => { - it("should retrieve list of hosts", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const hosts: DockerHost[] = [ - { id: 1, name: "H1", hostAddress: "a", secure: false }, - ]; - - mockDb.getDockerHosts.mockImplementation(() => hosts as never[]); - - try { - const app = createApp(); - const req = new Request("http://localhost:3000/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toEqual(hosts); - - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts success", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with hosts array", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when retrieval fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - - mockDb.getDockerHosts.mockImplementationOnce(() => { - throw new Error("Fetch error"); - }); - - try { - const app = createApp(); - const req = new Request("http://localhost:3000/docker-config/hosts"); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response).toMatchObject({ - body: expect.any(String), - }); - - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "get-hosts failure", - suite: "Docker Config - List Hosts", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); - - describe("DELETE /docker-config/hosts/:id", () => { - it("should delete a host successfully", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 5; - - try { - const app = createApp(); - const req = new Request( - `http://localhost:3000/docker-config/hosts/${id}`, - { - method: "DELETE", - }, - ); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(200); - expect(context.response.body).toMatchObject({ - message: `Deleted docker host (${id})`, - }); - expect(mockDb.deleteDockerHost).toHaveBeenCalledWith(id); - - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host success", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "200 OK with deletion confirmation", - received: context?.response, - }, - }); - throw error; - } - }); - - it("should handle error when delete fails", async () => { - const start = Date.now(); - let context: TestContext | undefined; - const id = 6; - - mockDb.deleteDockerHost.mockImplementationOnce(() => { - throw new Error("Delete error"); - }); - - try { - const app = createApp(); - const req = new Request( - `http://localhost:3000/docker-config/hosts/${id}`, - { - method: "DELETE", - }, - ); - const res = await app.handle(req); - context = await captureTestContext(req, res); - - expect(res.status).toBe(500); - expect(context.response).toMatchObject({ - body: expect.any(String), - }); - - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - context, - }); - } catch (error) { - recordTestResult({ - name: "delete-host failure", - suite: "Docker Config - Delete Host", - time: Date.now() - start, - error: error as Error, - context, - errorDetails: { - expected: "400 Error with error details", - received: context?.response, - }, - }); - throw error; - } - }); - }); -}); - -afterAll(() => { - generateMarkdownReport(); -}); diff --git a/src/tests/markdown-exporter.ts b/src/tests/markdown-exporter.ts deleted file mode 100644 index 2d55b48e..00000000 --- a/src/tests/markdown-exporter.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import { format } from "date-fns"; -import { logger } from "~/core/utils/logger"; - -export type TestContext = { - request: { - method: string; - url: string; - headers: Record; - query?: Record; - body?: unknown; - }; - response: { - status: number; - headers: Record; - body?: unknown; - }; -}; - -type ErrorDetails = { - expected?: unknown; - received?: unknown; -}; - -type TestResult = { - name: string; - suite: string; - time: number; - error?: Error; - context?: TestContext; - errorDetails?: ErrorDetails; -}; - -export function recordTestResult(result: TestResult) { - logger.debug(`__UT__ Recording test result: ${JSON.stringify(result)}`); - testResults.push(result); -} - -export const testResults: TestResult[] = []; - -function formatContextMarkdown( - context?: TestContext, - errorDetails?: ErrorDetails, -): string { - if (!context) return ""; - - let md = "```\n"; - md += "=== REQUEST ===\n"; - md += `Method: ${context.request.method}\n`; - md += `URL: ${context.request.url}\n`; - if (context.request.query) { - md += `Query Params: ${JSON.stringify(context.request.query, null, 2)}\n`; - } - md += `Headers: ${JSON.stringify(context.request.headers, null, 2)}\n`; - if (context.request.body) { - md += `Body: ${JSON.stringify(context.request.body, null, 2)}\n`; - } - md += "\n=== RESPONSE ===\n"; - md += `Status: ${context.response.status}\n`; - md += `Headers: ${JSON.stringify(context.response.headers, null, 2)}\n`; - if (context.response.body) { - md += `Body: ${JSON.stringify(context.response.body, null, 2)}\n`; - } - if (errorDetails) { - md += "\n=== ERROR DETAILS ===\n"; - md += `Expected: ${JSON.stringify(errorDetails.expected, null, 2)}\n`; - md += `Received: ${JSON.stringify(errorDetails.received, null, 2)}\n`; - } - md += "```\n"; - return md; -} - -export function generateMarkdownReport() { - if (testResults.length === 0) { - logger.warn("No test results to generate markdown report."); - return; - } - - const totalTests = testResults.length; - const totalErrors = testResults.filter((r) => r.error).length; - - const testSuites = testResults.reduce( - (suites, result) => { - if (!suites[result.suite]) { - suites[result.suite] = []; - } - suites[result.suite].push(result); - return suites; - }, - {} as Record, - ); - - let md = `# Test Report - ${format(new Date(), "yyyy-MM-dd")}\n`; - md += `\n**Total Tests:** ${totalTests} -`; - md += `**Total Failures:** ${totalErrors}\n`; - - for (const [suiteName, cases] of Object.entries(testSuites)) { - const suiteErrors = cases.filter((c) => c.error).length; - md += `\n## Suite: ${suiteName} -`; - md += `- Tests: ${cases.length} -`; - md += `- Failures: ${suiteErrors}\n`; - - for (const test of cases) { - const status = test.error ? "❌ Failed" : "✅ Passed"; - md += `\n### ${test.name} (${(test.time / 1000).toFixed(2)}s) -`; - md += `- Status: **${status}** \n`; - - if (test.error) { - const msg = test.error.message - .replace(//g, ">"); - const stack = test.error.stack - ?.replace(//g, ">"); - md += "\n
\nError Details\n\n"; - md += `**Message:** ${msg} \n`; - if (stack) { - md += `\n\`\`\`\n${stack}\n\`\`\`\n`; - } - md += "
\n"; - } - - if (test.context) { - md += "\n
\nRequest/Response Context\n\n"; - md += formatContextMarkdown(test.context, test.errorDetails); - md += "
\n"; - } - } - } - - // Ensure directory exists - mkdirSync("reports/markdown", { recursive: true }); - const filename = `reports/markdown/test-report-${format( - new Date(), - "yyyy-MM-dd", - )}.md`; - writeFileSync(filename, md, "utf8"); - - logger.debug(`__UT__ Markdown report written to ${filename}`); -} diff --git a/src/typings b/src/typings index 9cae829b..d0d22fa6 160000 --- a/src/typings +++ b/src/typings @@ -1 +1 @@ -Subproject commit 9cae829bead60cd13351b757340f3225649cb11d +Subproject commit d0d22fa622c5dd9d298d358d4215c8b54cb5f4f3 From 21e3a804a5d419b54ff3252df93107dca3d64a26 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 29 Jun 2025 17:37:21 +0200 Subject: [PATCH 337/369] Refactor(handlers): Standardize formatting and imports This commit improves code readability and consistency across handler files by: - Standardizing import statements to be grouped and ordered. - Ensuring consistent spacing and formatting within the code. --- src/handlers/config.ts | 368 +++++++++++++++++++-------------------- src/handlers/database.ts | 12 +- src/handlers/docker.ts | 294 +++++++++++++++---------------- src/handlers/index.ts | 14 +- src/handlers/logs.ts | 78 ++++----- src/handlers/stacks.ts | 238 ++++++++++++------------- 6 files changed, 502 insertions(+), 502 deletions(-) diff --git a/src/handlers/config.ts b/src/handlers/config.ts index 08b0ae16..e55cc628 100644 --- a/src/handlers/config.ts +++ b/src/handlers/config.ts @@ -1,193 +1,193 @@ +import { existsSync, readdirSync, unlinkSync } from "node:fs"; import { dbFunctions } from "~/core/database"; -import type { config } from "~/typings/database"; -import { logger } from "~/core/utils/logger"; -import { pluginManager } from "~/core/plugins/plugin-manager"; -import { hashApiKey } from "~/middleware/auth"; import { backupDir } from "~/core/database/backup"; +import { pluginManager } from "~/core/plugins/plugin-manager"; +import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; -import { existsSync, readdirSync, unlinkSync } from "node:fs"; -import { DockerHost } from "~/typings/docker"; +import { hashApiKey } from "~/middleware/auth"; +import type { config } from "~/typings/database"; +import type { DockerHost } from "~/typings/docker"; class apiHandler { - async getConfig() { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateConfig( - fetching_interval: number, - keep_data_for: number, - api_key: string - ) { - try { - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key) - ); - return "Updated DockStatAPI config"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getPlugins() { - try { - return pluginManager.getPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getPackage() { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json` - ); - - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } - - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async createbackup() { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return backupFilename; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async listBackups() { - try { - const backupFiles = readdirSync(backupDir); - - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.startsWith(".") || - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); - - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async downloadbackup(downloadFile?: string) { - try { - const filename: string = downloadFile || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; - - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } - - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async restoreBackup(file: Bun.FileBlob) { - try { - if (!file) { - throw new Error("No file uploaded"); - } - - if (!(file.name || "").endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } - - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); - - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); - - return "Database restored successfully"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async addHost(host: DockerHost) { - try { - dbFunctions.addDockerHost(host); - return `Added docker host (${host.name} - ${host.hostAddress})`; - } catch (error: unknown) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateHost(host: DockerHost) { - try { - dbFunctions.updateDockerHost(host); - return `Updated docker host (${host.id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async removeHost(id: number) { - try { - dbFunctions.deleteDockerHost(id); - return `Deleted docker host (${id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } + async getConfig() { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateConfig( + fetching_interval: number, + keep_data_for: number, + api_key: string, + ) { + try { + dbFunctions.updateConfig( + fetching_interval, + keep_data_for, + await hashApiKey(api_key), + ); + return "Updated DockStatAPI config"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getPlugins() { + try { + return pluginManager.getPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getPackage() { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, + ); + + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } + + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async createbackup() { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return backupFilename; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async listBackups() { + try { + const backupFiles = readdirSync(backupDir); + + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.startsWith(".") || + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); + + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async downloadbackup(downloadFile?: string) { + try { + const filename: string = downloadFile || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; + + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } + + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async restoreBackup(file: Bun.FileBlob) { + try { + if (!file) { + throw new Error("No file uploaded"); + } + + if (!(file.name || "").endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } + + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return "Database restored successfully"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async addHost(host: DockerHost) { + try { + dbFunctions.addDockerHost(host); + return `Added docker host (${host.name} - ${host.hostAddress})`; + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateHost(host: DockerHost) { + try { + dbFunctions.updateDockerHost(host); + return `Updated docker host (${host.id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async removeHost(id: number) { + try { + dbFunctions.deleteDockerHost(id); + return `Deleted docker host (${id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } } export const ApiHandler = new apiHandler(); diff --git a/src/handlers/database.ts b/src/handlers/database.ts index 6fb95f03..d6bd0c49 100644 --- a/src/handlers/database.ts +++ b/src/handlers/database.ts @@ -1,13 +1,13 @@ import { dbFunctions } from "~/core/database"; class databaseHandler { - async getContainers() { - return dbFunctions.getContainerStats(); - } + async getContainers() { + return dbFunctions.getContainerStats(); + } - async getHosts() { - return dbFunctions.getHostStats(); - } + async getHosts() { + return dbFunctions.getHostStats(); + } } export const DatabaseHandler = new databaseHandler(); diff --git a/src/handlers/docker.ts b/src/handlers/docker.ts index 9c7a6251..7606f56d 100644 --- a/src/handlers/docker.ts +++ b/src/handlers/docker.ts @@ -1,156 +1,156 @@ +import type Docker from "dockerode"; import { dbFunctions } from "~/core/database"; -import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import { getDockerClient } from "~/core/docker/client"; -import type Docker from "dockerode"; +import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; +import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; -import { findObjectByKey } from "~/core/utils/helpers"; class basicDockerHandler { - async getContainers() { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; - - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - throw new Error(pingError as string); - } - - const hostContainers = await docker.listContainers({ all: true }); - - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - throw new Error(error as string); - } - if (!stats) { - throw new Error("No stats available"); - } - resolve(stats); - }); - } - ); - - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: stats.cpu_stats.system_cpu_usage, - memoryUsage: stats.memory_stats.usage, - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError - ); - } - }) - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }) - ); - - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getHostStats(id?: number) { - if (!id) { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - - const stats: HostStats[] = []; - - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - stats.push(config); - } - - logger.debug("Fetched all hosts"); - return stats; - } catch (error) { - throw new Error(error as string); - } - } - - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - - const host = findObjectByKey(hosts, "id", Number(id)); - if (!host) { - throw new Error(`Host (${id}) not found`); - } - - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - throw new Error("Failed to retrieve host config"); - } - } + async getContainers() { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; + + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + throw new Error(pingError as string); + } + + const hostContainers = await docker.listContainers({ all: true }); + + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + throw new Error(error as string); + } + if (!stats) { + throw new Error("No stats available"); + } + resolve(stats); + }); + }, + ); + + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: stats.cpu_stats.system_cpu_usage, + memoryUsage: stats.memory_stats.usage, + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }), + ); + + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getHostStats(id?: number) { + if (!id) { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + const stats: HostStats[] = []; + + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + stats.push(config); + } + + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + throw new Error(error as string); + } + } + + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + const host = findObjectByKey(hosts, "id", Number(id)); + if (!host) { + throw new Error(`Host (${id}) not found`); + } + + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + throw new Error("Failed to retrieve host config"); + } + } } export const BasicDockerHandler = new basicDockerHandler(); diff --git a/src/handlers/index.ts b/src/handlers/index.ts index aaa64662..f08ee247 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -1,13 +1,13 @@ -import { BasicDockerHandler } from "./docker"; import { ApiHandler } from "./config"; import { DatabaseHandler } from "./database"; -import { StackHandler } from "./stacks"; +import { BasicDockerHandler } from "./docker"; import { LogHandler } from "./logs"; +import { StackHandler } from "./stacks"; export const handlers = { - BasicDockerHandler, - ApiHandler, - DatabaseHandler, - StackHandler, - LogHandler, + BasicDockerHandler, + ApiHandler, + DatabaseHandler, + StackHandler, + LogHandler, }; diff --git a/src/handlers/logs.ts b/src/handlers/logs.ts index ffd01852..5ab74593 100644 --- a/src/handlers/logs.ts +++ b/src/handlers/logs.ts @@ -2,49 +2,49 @@ import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; class logHandler { - async getLogs(level?: string) { - if (!level) { - try { - const logs = dbFunctions.getAllLogs(); - logger.debug("Retrieved all logs"); - return logs; - } catch (error) { - logger.error("Failed to retrieve logs,", error); - throw new Error("Failed to retrieve logs"); - } - } - try { - const logs = dbFunctions.getLogsByLevel(level); + async getLogs(level?: string) { + if (!level) { + try { + const logs = dbFunctions.getAllLogs(); + logger.debug("Retrieved all logs"); + return logs; + } catch (error) { + logger.error("Failed to retrieve logs,", error); + throw new Error("Failed to retrieve logs"); + } + } + try { + const logs = dbFunctions.getLogsByLevel(level); - logger.debug(`Retrieved logs (level: ${level})`); - return logs; - } catch (error) { - logger.error("Failed to retrieve logs"); - throw new Error(`Failed to retrieve logs`); - } - } + logger.debug(`Retrieved logs (level: ${level})`); + return logs; + } catch (error) { + logger.error("Failed to retrieve logs"); + throw new Error("Failed to retrieve logs"); + } + } - async deleteLogs(level?: string) { - if (!level) { - try { - dbFunctions.clearAllLogs(); - return { success: true }; - } catch (error) { - logger.error("Could not delete all logs,", error); - throw new Error("Could not delete all logs"); - } - } + async deleteLogs(level?: string) { + if (!level) { + try { + dbFunctions.clearAllLogs(); + return { success: true }; + } catch (error) { + logger.error("Could not delete all logs,", error); + throw new Error("Could not delete all logs"); + } + } - try { - dbFunctions.clearLogsByLevel(level); + try { + dbFunctions.clearLogsByLevel(level); - logger.debug(`Cleared all logs with level: ${level}`); - return { success: true }; - } catch (error) { - logger.error("Could not clear logs with level", level, ",", error); - throw new Error("Failed to retrieve logs"); - } - } + logger.debug(`Cleared all logs with level: ${level}`); + return { success: true }; + } catch (error) { + logger.error("Could not clear logs with level", level, ",", error); + throw new Error("Failed to retrieve logs"); + } + } } export const LogHandler = new logHandler(); diff --git a/src/handlers/stacks.ts b/src/handlers/stacks.ts index 09a8a5db..abdbb8e0 100644 --- a/src/handlers/stacks.ts +++ b/src/handlers/stacks.ts @@ -1,127 +1,127 @@ +import { dbFunctions } from "~/core/database"; import { - deployStack, - getAllStacksStatus, - getStackStatus, - pullStackImages, - removeStack, - restartStack, - startStack, - stopStack, + deployStack, + getAllStacksStatus, + getStackStatus, + pullStackImages, + removeStack, + restartStack, + startStack, + stopStack, } from "~/core/stacks/controller"; -import type { stacks_config } from "~/typings/database"; import { logger } from "~/core/utils/logger"; -import { dbFunctions } from "~/core/database"; +import type { stacks_config } from "~/typings/database"; class stackHandler { - async deploy(config: stacks_config) { - try { - await deployStack(config); - logger.info(`Deployed Stack (${config.name})`); - return `Stack ${config.name} deployed successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error deploying stack, please check the server logs for more information`; - } - } - - async start(stackId: number) { - try { - if (!stackId) { - throw new Error("Stack ID needed"); - } - await startStack(stackId); - logger.info(`Started Stack (${stackId})`); - return `Stack ${stackId} started successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error starting stack`; - } - } - - async stop(stackId: number) { - try { - if (!stackId) { - throw new Error("Stack needed"); - } - await stopStack(stackId); - logger.info(`Stopped Stack (${stackId})`); - return `Stack ${stackId} stopped successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error stopping stack`; - } - } - - async restart(stackId: number) { - try { - if (!stackId) { - throw new Error("StackID needed"); - } - await restartStack(stackId); - logger.info(`Restarted Stack (${stackId})`); - return `Stack ${stackId} restarted successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error restarting stack`; - } - } - - async pullImages(stackId: number) { - try { - if (!stackId) { - throw new Error("StackID needed"); - } - await pullStackImages(stackId); - logger.info(`Pulled Stack images (${stackId})`); - return `Images for stack ${stackId} pulled successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error pulling images`; - } - } - - async getStatus(stackId?: number) { - if (stackId) { - const status = await getStackStatus(stackId); - logger.debug( - `Retrieved status for stackId=${stackId}: ${JSON.stringify(status)}` - ); - return status; - } - - logger.debug("Fetching status for all stacks"); - const status = await getAllStacksStatus(); - logger.debug(`Retrieved status for all stacks: ${JSON.stringify(status)}`); - - return status; - } - - async listStacks() { - try { - const stacks = dbFunctions.getStacks(); - logger.info("Fetched Stacks"); - return stacks; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - return `${errorMsg}, Error getting stacks`; - } - } - - async deleteStack(stackId: number) { - try { - await removeStack(stackId); - logger.info(`Deleted Stack ${stackId}`); - return `Stack ${stackId} deleted successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - return `${errorMsg}, Error deleting stack`; - } - } + async deploy(config: stacks_config) { + try { + await deployStack(config); + logger.info(`Deployed Stack (${config.name})`); + return `Stack ${config.name} deployed successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error deploying stack, please check the server logs for more information`; + } + } + + async start(stackId: number) { + try { + if (!stackId) { + throw new Error("Stack ID needed"); + } + await startStack(stackId); + logger.info(`Started Stack (${stackId})`); + return `Stack ${stackId} started successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error starting stack`; + } + } + + async stop(stackId: number) { + try { + if (!stackId) { + throw new Error("Stack needed"); + } + await stopStack(stackId); + logger.info(`Stopped Stack (${stackId})`); + return `Stack ${stackId} stopped successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error stopping stack`; + } + } + + async restart(stackId: number) { + try { + if (!stackId) { + throw new Error("StackID needed"); + } + await restartStack(stackId); + logger.info(`Restarted Stack (${stackId})`); + return `Stack ${stackId} restarted successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error restarting stack`; + } + } + + async pullImages(stackId: number) { + try { + if (!stackId) { + throw new Error("StackID needed"); + } + await pullStackImages(stackId); + logger.info(`Pulled Stack images (${stackId})`); + return `Images for stack ${stackId} pulled successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error pulling images`; + } + } + + async getStatus(stackId?: number) { + if (stackId) { + const status = await getStackStatus(stackId); + logger.debug( + `Retrieved status for stackId=${stackId}: ${JSON.stringify(status)}`, + ); + return status; + } + + logger.debug("Fetching status for all stacks"); + const status = await getAllStacksStatus(); + logger.debug(`Retrieved status for all stacks: ${JSON.stringify(status)}`); + + return status; + } + + async listStacks() { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks"); + return stacks; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return `${errorMsg}, Error getting stacks`; + } + } + + async deleteStack(stackId: number) { + try { + await removeStack(stackId); + logger.info(`Deleted Stack ${stackId}`); + return `Stack ${stackId} deleted successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return `${errorMsg}, Error deleting stack`; + } + } } export const StackHandler = new stackHandler(); From d3601a578d813bfc61f4f4b63ee0faec97cad891 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Mon, 30 Jun 2025 00:53:29 +0200 Subject: [PATCH 338/369] feat(config): Remove API key from config and database This commit removes the API key functionality from the DockStatAPI. The API key column is removed from the config table in the database, and the updateConfig function no longer accepts or updates the API key. This change simplifies the configuration and security of the application. The Docker websocket route and auth middleware was removed, since it is not used anymore. BREAKING CHANGE: The API key functionality has been removed. Users will no longer be able to set or validate an API key. The config endpoint takes only keep_data_for and fetching_interval now. --- src/core/database/config.ts | 14 +- src/core/database/database.ts | 6 +- src/core/docker/scheduler.ts | 204 +++++++-------- src/core/stacks/controller.ts | 2 +- src/core/utils/logger.ts | 2 +- src/core/utils/response-handler.ts | 42 ---- src/core/utils/swagger-readme.ts | 66 ----- src/handlers/config.ts | 349 +++++++++++++------------- src/handlers/index.ts | 16 +- src/handlers/modules/docker-socket.ts | 143 +++++++++++ src/handlers/modules/live-stacks.ts | 43 ++++ src/handlers/modules/logs-socket.ts | 53 ++++ src/handlers/sockets.ts | 9 + src/handlers/utils.ts | 3 + src/middleware/auth.ts | 89 ------- src/routes/docker-websocket.ts | 136 ---------- src/routes/live-logs.ts | 38 --- src/routes/live-stacks.ts | 30 --- 18 files changed, 546 insertions(+), 699 deletions(-) delete mode 100644 src/core/utils/response-handler.ts delete mode 100644 src/core/utils/swagger-readme.ts create mode 100644 src/handlers/modules/docker-socket.ts create mode 100644 src/handlers/modules/live-stacks.ts create mode 100644 src/handlers/modules/logs-socket.ts create mode 100644 src/handlers/sockets.ts create mode 100644 src/handlers/utils.ts delete mode 100644 src/middleware/auth.ts delete mode 100644 src/routes/docker-websocket.ts delete mode 100644 src/routes/live-logs.ts delete mode 100644 src/routes/live-stacks.ts diff --git a/src/core/database/config.ts b/src/core/database/config.ts index f2460e06..0fa66da7 100644 --- a/src/core/database/config.ts +++ b/src/core/database/config.ts @@ -3,11 +3,9 @@ import { executeDbOperation } from "./helper"; const stmt = { update: db.prepare( - "UPDATE config SET fetching_interval = ?, keep_data_for = ?, api_key = ?", - ), - select: db.prepare( - "SELECT keep_data_for, fetching_interval, api_key FROM config", + "UPDATE config SET fetching_interval = ?, keep_data_for = ?", ), + select: db.prepare("SELECT keep_data_for, fetching_interval FROM config"), deleteOld: db.prepare( `DELETE FROM container_stats WHERE timestamp < datetime('now', '-' || ? || ' days')`, ), @@ -16,14 +14,10 @@ const stmt = { ), }; -export function updateConfig( - fetching_interval: number, - keep_data_for: number, - api_key: string, -) { +export function updateConfig(fetching_interval: number, keep_data_for: number) { return executeDbOperation( "Update Config", - () => stmt.update.run(fetching_interval, keep_data_for, api_key), + () => stmt.update.run(fetching_interval, keep_data_for), () => { if ( typeof fetching_interval !== "number" || diff --git a/src/core/database/database.ts b/src/core/database/database.ts index 204666ef..3a9311c3 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -90,9 +90,7 @@ export function init() { CREATE TABLE IF NOT EXISTS config ( keep_data_for NUMBER NOT NULL, - fetching_interval NUMBER NOT NULL, - api_key TEXT NOT NULL - ); + fetching_interval NUMBER NOT NULL ); `); const configRow = db @@ -101,7 +99,7 @@ export function init() { if (configRow.count === 0) { db.prepare( - 'INSERT INTO config (keep_data_for, fetching_interval, api_key) VALUES (7, 5, "changeme")', + "INSERT INTO config (keep_data_for, fetching_interval) VALUES (7, 5)", ).run(); } diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index 8682411b..480fe0f4 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -5,111 +5,119 @@ import { logger } from "~/core/utils/logger"; import type { config } from "~/typings/database"; function convertFromMinToMs(minutes: number): number { - return minutes * 60 * 1000; + return minutes * 60 * 1000; } async function initialRun( - scheduleName: string, - scheduleFunction: Promise | void, - isAsync: boolean, + scheduleName: string, + scheduleFunction: Promise | void, + isAsync: boolean ) { - try { - if (isAsync) { - await scheduleFunction; - } else { - scheduleFunction; - } - logger.info(`Startup run success for: ${scheduleName}`); - } catch (error) { - logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); - } + try { + if (isAsync) { + await scheduleFunction; + } else { + scheduleFunction; + } + logger.info(`Startup run success for: ${scheduleName}`); + } catch (error) { + logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); + } +} + +async function scheduledJob( + name: string, + jobFn: () => Promise, + intervalMs: number +) { + while (true) { + const start = Date.now(); + logger.info(`Task Start: ${name}`); + try { + await jobFn(); + logger.info(`Task End: ${name} succeeded.`); + } catch (e) { + logger.error(`Task End: ${name} failed:`, e); + } + const elapsed = Date.now() - start; + const delay = Math.max(0, intervalMs - elapsed); + await new Promise((r) => setTimeout(r, delay)); + } } async function setSchedules() { - try { - const rawConfigData: unknown[] = dbFunctions.getConfig(); - const configData = rawConfigData[0]; - - if ( - !configData || - typeof (configData as config).keep_data_for !== "number" || - typeof (configData as config).fetching_interval !== "number" - ) { - logger.error("Invalid configuration data:", configData); - throw new Error("Invalid configuration data"); - } - - const { keep_data_for, fetching_interval } = configData as config; - - if (keep_data_for === undefined) { - const errMsg = "keep_data_for is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } - - if (fetching_interval === undefined) { - const errMsg = "fetching_interval is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } - - logger.info( - `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, - ); - - logger.info( - `Scheduling: Updating host statistics every ${fetching_interval} minutes`, - ); - - logger.info( - `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, - ); - - // Schedule container data fetching - await initialRun("storeContainerData", storeContainerData(), true); - setInterval(async () => { - try { - logger.info("Task Start: Fetching container data."); - await storeContainerData(); - logger.info("Task End: Container data fetched successfully."); - } catch (error) { - logger.error("Error in fetching container data:", error); - } - }, convertFromMinToMs(fetching_interval)); - - // Schedule Host statistics updates - await initialRun("storeHostData", storeHostData(), true); - setInterval(async () => { - try { - logger.info("Task Start: Updating host stats."); - await storeHostData(); - logger.info("Task End: Updating host stats successfully."); - } catch (error) { - logger.error("Error in updating host stats:", error); - } - }, convertFromMinToMs(fetching_interval)); - - // Schedule database cleanup - await initialRun( - "dbFunctions.deleteOldData", - dbFunctions.deleteOldData(keep_data_for), - false, - ); - setInterval(() => { - try { - logger.info("Task Start: Cleaning up old database data."); - dbFunctions.deleteOldData(keep_data_for); - logger.info("Task End: Database cleanup completed."); - } catch (error) { - logger.error("Error in database cleanup task:", error); - } - }, convertFromMinToMs(60)); - - logger.info("Schedules have been set successfully."); - } catch (error) { - logger.error("Error setting schedules:", error); - throw error; - } + try { + const rawConfigData: unknown[] = dbFunctions.getConfig(); + const configData = rawConfigData[0]; + + if ( + !configData || + typeof (configData as config).keep_data_for !== "number" || + typeof (configData as config).fetching_interval !== "number" + ) { + logger.error("Invalid configuration data:", configData); + throw new Error("Invalid configuration data"); + } + + const { keep_data_for, fetching_interval } = configData as config; + + if (keep_data_for === undefined) { + const errMsg = "keep_data_for is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + if (fetching_interval === undefined) { + const errMsg = "fetching_interval is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + logger.info( + `Scheduling: Fetching container statistics every ${fetching_interval} minutes` + ); + + logger.info( + `Scheduling: Updating host statistics every ${fetching_interval} minutes` + ); + + logger.info( + `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days` + ); + + // Schedule container data fetching + await initialRun("storeContainerData", storeContainerData(), true); + scheduledJob( + "storeContainerData", + storeContainerData, + convertFromMinToMs(fetching_interval) + ); + + // Schedule Host statistics updates + await initialRun("storeHostData", storeHostData(), true); + scheduledJob( + "storeHostData", + storeHostData, + convertFromMinToMs(fetching_interval) + ); + + // Schedule database cleanup + await initialRun( + "dbFunctions.deleteOldData", + dbFunctions.deleteOldData(keep_data_for), + false + ); + scheduledJob( + "cleanupOldData", + () => Promise.resolve(dbFunctions.deleteOldData(keep_data_for)), + convertFromMinToMs(60) + ); + + logger.info("Schedules have been set successfully."); + } catch (error) { + logger.error("Error setting schedules:", error); + throw error; + } } export { setSchedules }; diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 90f7c671..193c6a26 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -2,7 +2,7 @@ import { rm } from "node:fs/promises"; import DockerCompose from "docker-compose"; import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; -import { postToClient } from "~/routes/live-stacks"; +import { postToClient } from "~/handlers/modules/live-stacks"; import type { stacks_config } from "~/typings/database"; import type { Stack } from "~/typings/docker-compose"; import type { ComposeSpec } from "~/typings/docker-compose"; diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index f9304ab1..1f16f809 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -6,7 +6,7 @@ import wrapAnsi from "wrap-ansi"; import { dbFunctions } from "~/core/database"; -import { logToClients } from "~/routes/live-logs"; +import { logToClients } from "~/handlers/modules/logs-socket"; import type { log_message } from "~/typings/database"; diff --git a/src/core/utils/response-handler.ts b/src/core/utils/response-handler.ts deleted file mode 100644 index 00d5b464..00000000 --- a/src/core/utils/response-handler.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { logger } from "~/core/utils/logger"; -import type { set } from "~/typings/elysiajs"; - -export const responseHandler = { - error( - set: set, - error: string, - response_message: string, - error_code?: number, - ) { - set.status = error_code || 500; - logger.error(`${response_message} - ${error}`); - return { success: false, message: response_message, error: String(error) }; - }, - - ok(set: set, response_message: string) { - set.status = 200; - logger.debug(response_message); - return { success: true, message: response_message }; - }, - - simple_error(set: set, response_message: string, status_code?: number) { - set.status = status_code || 502; - logger.warn(response_message); - return { success: false, message: response_message }; - }, - - reject( - set: set, - reject: CallableFunction, - response_message: string, - error?: string, - ) { - set.status = 501; - if (error) { - logger.error(`${response_message} - ${error}`); - } else { - logger.error(response_message); - } - return reject(new Error(response_message)); - }, -}; diff --git a/src/core/utils/swagger-readme.ts b/src/core/utils/swagger-readme.ts deleted file mode 100644 index c1457c68..00000000 --- a/src/core/utils/swagger-readme.ts +++ /dev/null @@ -1,66 +0,0 @@ -export const swaggerReadme: string = ` -[Download API type sheet](/server.d.ts) - -![Docker](https://img.shields.io/badge/Docker-2CA5E0?style=flat&logo=docker&logoColor=white) -![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white) - -Docker infrastructure management API with real-time monitoring and orchestration capabilities. - -## Key Features - -- **Stack Orchestration** - Deploy/update Docker stacks (compose v3+) with custom configurations -- **Container Monitoring** - Real-time metrics (CPU/RAM/status) across multiple Docker hosts -- **Centralized Logging** - Structured log management with retention policies and filtering -- **Host Management** - Multi-host configuration with connection health checks -- **Plugin System** - Extensible architecture for custom monitoring integrations - -## Installation & Setup - -**Prerequisites**: -- Node.js 18+ -- Docker Engine 23+ -- Bun runtime - -\`\`\`bash -# Clone repo -git clone https://github.com/Its4Nik/DockStatAPI.git -cd DockStatAPI -# Install dependencies -bun install - -# Start development server -bun run dev -\`\`\` - -## Configuration - -**Environment Variables**: -\`\`\`ini -PAD_NEW_LINES=true -NODE_ENV=production -LOG_LEVEL=info -\`\`\` - -## Security - -1. Always use HTTPS in production -2. Rotate API keys regularly -3. Restrict host connections to trusted networks -4. Enable Docker Engine TLS authentication - -## Contributing - -1. Fork repository -2. Create feature branch (\`feat/my-feature\`) -3. Submit PR with detailed description - -**Code Style**: -- TypeScript strict mode -- Elysia framework conventions -- Prettier formatting -`; diff --git a/src/handlers/config.ts b/src/handlers/config.ts index e55cc628..1057da7f 100644 --- a/src/handlers/config.ts +++ b/src/handlers/config.ts @@ -4,190 +4,181 @@ import { backupDir } from "~/core/database/backup"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; -import { hashApiKey } from "~/middleware/auth"; import type { config } from "~/typings/database"; import type { DockerHost } from "~/typings/docker"; class apiHandler { - async getConfig() { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateConfig( - fetching_interval: number, - keep_data_for: number, - api_key: string, - ) { - try { - dbFunctions.updateConfig( - fetching_interval, - keep_data_for, - await hashApiKey(api_key), - ); - return "Updated DockStatAPI config"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getPlugins() { - try { - return pluginManager.getPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getPackage() { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json`, - ); - - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } - - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async createbackup() { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return backupFilename; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async listBackups() { - try { - const backupFiles = readdirSync(backupDir); - - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.startsWith(".") || - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); - - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async downloadbackup(downloadFile?: string) { - try { - const filename: string = downloadFile || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; - - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } - - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async restoreBackup(file: Bun.FileBlob) { - try { - if (!file) { - throw new Error("No file uploaded"); - } - - if (!(file.name || "").endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } - - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); - - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); - - return "Database restored successfully"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async addHost(host: DockerHost) { - try { - dbFunctions.addDockerHost(host); - return `Added docker host (${host.name} - ${host.hostAddress})`; - } catch (error: unknown) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateHost(host: DockerHost) { - try { - dbFunctions.updateDockerHost(host); - return `Updated docker host (${host.id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async removeHost(id: number) { - try { - dbFunctions.deleteDockerHost(id); - return `Deleted docker host (${id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } + async getConfig() { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateConfig(fetching_interval: number, keep_data_for: number) { + try { + dbFunctions.updateConfig(fetching_interval, keep_data_for); + return "Updated DockStatAPI config"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getPlugins() { + try { + return pluginManager.getPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getPackage() { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json` + ); + + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } + + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async createbackup() { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return backupFilename; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async listBackups() { + try { + const backupFiles = readdirSync(backupDir); + + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.startsWith(".") || + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); + + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async downloadbackup(downloadFile?: string) { + try { + const filename: string = downloadFile || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; + + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } + + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async restoreBackup(file: Bun.FileBlob) { + try { + if (!file) { + throw new Error("No file uploaded"); + } + + if (!(file.name || "").endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } + + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return "Database restored successfully"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async addHost(host: DockerHost) { + try { + dbFunctions.addDockerHost(host); + return `Added docker host (${host.name} - ${host.hostAddress})`; + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateHost(host: DockerHost) { + try { + dbFunctions.updateDockerHost(host); + return `Updated docker host (${host.id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async removeHost(id: number) { + try { + dbFunctions.deleteDockerHost(id); + return `Deleted docker host (${id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } } export const ApiHandler = new apiHandler(); diff --git a/src/handlers/index.ts b/src/handlers/index.ts index f08ee247..43fc11ba 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -1,13 +1,19 @@ +import { setSchedules } from "~/core/docker/scheduler"; import { ApiHandler } from "./config"; import { DatabaseHandler } from "./database"; import { BasicDockerHandler } from "./docker"; import { LogHandler } from "./logs"; +import { Sockets } from "./sockets"; import { StackHandler } from "./stacks"; +import { CheckHealth } from "./utils"; export const handlers = { - BasicDockerHandler, - ApiHandler, - DatabaseHandler, - StackHandler, - LogHandler, + BasicDockerHandler, + ApiHandler, + DatabaseHandler, + StackHandler, + LogHandler, + CheckHealth, + sockets: Sockets, + start: setSchedules, }; diff --git a/src/handlers/modules/docker-socket.ts b/src/handlers/modules/docker-socket.ts new file mode 100644 index 00000000..080ee2bd --- /dev/null +++ b/src/handlers/modules/docker-socket.ts @@ -0,0 +1,143 @@ +import { Readable, type Transform } from "node:stream"; +import split2 from "split2"; +import { dbFunctions } from "~/core/database"; +import { getDockerClient } from "~/core/docker/client"; +import { + calculateCpuPercent, + calculateMemoryUsage, +} from "~/core/utils/calculations"; +import { logger } from "~/core/utils/logger"; +import type { DockerStatsEvent } from "~/typings/docker"; +import { ContainerInfo } from "~/typings/docker"; + +export function createDockerStatsStream(): Readable { + const stream = new Readable({ + objectMode: true, + read() {}, + }); + + const substreams: Array<{ + statsStream: Readable; + splitStream: Transform; + }> = []; + + const cleanup = () => { + for (const { statsStream, splitStream } of substreams) { + try { + statsStream.unpipe(splitStream); + statsStream.destroy(); + splitStream.destroy(); + } catch (error) { + logger.error(`Cleanup error: ${error}`); + } + } + substreams.length = 0; + }; + + stream.on("close", cleanup); + stream.on("error", cleanup); + + (async () => { + try { + const hosts = dbFunctions.getDockerHosts(); + logger.debug(`Retrieved ${hosts.length} docker host(s)`); + + for (const host of hosts) { + if (stream.destroyed) break; + + try { + const docker = getDockerClient(host); + await docker.ping(); + const containers = await docker.listContainers({ + all: true, + }); + + logger.debug( + `Found ${containers.length} containers on ${host.name} (id: ${host.id})`, + ); + + for (const containerInfo of containers) { + if (stream.destroyed) break; + + try { + const container = docker.getContainer(containerInfo.Id); + const statsStream = (await container.stats({ + stream: true, + })) as Readable; + const splitStream = split2(); + + substreams.push({ statsStream, splitStream }); + + statsStream + .on("close", () => splitStream.destroy()) + .pipe(splitStream) + .on("data", (line: string) => { + if (stream.destroyed || !line) return; + + try { + const stats = JSON.parse(line); + const event: DockerStatsEvent = { + type: "stats", + id: containerInfo.Id, + hostId: host.id, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats) ?? 0, + memoryUsage: calculateMemoryUsage(stats) ?? 0, + }; + stream.push(event); + } catch (error) { + stream.push({ + type: "error", + hostId: host.id, + containerId: containerInfo.Id, + error: `Parse error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + }) + .on("error", (error: Error) => { + stream.push({ + type: "error", + hostId: host.id, + containerId: containerInfo.Id, + error: `Stream error: ${error.message}`, + }); + }); + } catch (error) { + stream.push({ + type: "error", + hostId: host.id, + containerId: containerInfo.Id, + error: `Container error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + } + } catch (error) { + stream.push({ + type: "error", + hostId: host.id, + error: `Host connection error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + } + } catch (error) { + stream.push({ + type: "error", + error: `Initialization error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + stream.destroy(); + } + })(); + + return stream; +} diff --git a/src/handlers/modules/live-stacks.ts b/src/handlers/modules/live-stacks.ts new file mode 100644 index 00000000..924d76ab --- /dev/null +++ b/src/handlers/modules/live-stacks.ts @@ -0,0 +1,43 @@ +import { PassThrough, type Readable } from "node:stream"; +import { logger } from "~/core/utils/logger"; +import type { stackSocketMessage } from "~/typings/websocket"; + +const activeStreams = new Set(); + +export function createStackStream(): Readable { + const stream = new PassThrough({ objectMode: true }); + + activeStreams.add(stream); + logger.info( + `New Stack stream created. Active streams: ${activeStreams.size}`, + ); + + const removeStream = () => { + if (activeStreams.delete(stream)) { + logger.info(`Stack stream closed. Active streams: ${activeStreams.size}`); + if (!stream.destroyed) { + stream.destroy(); + } + } + }; + + stream.on("close", removeStream); + stream.on("end", removeStream); + stream.on("error", (error) => { + logger.error(`Stream error: ${error.message}`); + removeStream(); + }); + + return stream; +} + +export function postToClient(stackMessage: stackSocketMessage) { + for (const stream of activeStreams) { + try { + stream.push(JSON.stringify(stackMessage)); + } catch (error) { + activeStreams.delete(stream); + logger.error("Failed to send to Socket:", error); + } + } +} diff --git a/src/handlers/modules/logs-socket.ts b/src/handlers/modules/logs-socket.ts new file mode 100644 index 00000000..77a730e0 --- /dev/null +++ b/src/handlers/modules/logs-socket.ts @@ -0,0 +1,53 @@ +import { PassThrough, type Readable } from "node:stream"; +import { logger } from "~/core/utils/logger"; +import type { log_message } from "~/typings/database"; + +const activeStreams = new Set(); + +export function createLogStream(): Readable { + const stream = new PassThrough({ objectMode: true }); + + activeStreams.add(stream); + logger.info(`New Logs stream created. Active streams: ${activeStreams.size}`); + + const removeStream = () => { + if (activeStreams.delete(stream)) { + logger.info(`Logs stream closed. Active streams: ${activeStreams.size}`); + if (!stream.destroyed) { + stream.destroy(); + } + } + }; + + stream.on("close", removeStream); + stream.on("end", removeStream); + stream.on("error", (error) => { + logger.error(`Stream error: ${error.message}`); + removeStream(); + }); + + return stream; +} + +export function logToClients(data: log_message): void { + for (const stream of activeStreams) { + try { + if (stream.writable && !stream.destroyed) { + const success = stream.write(data); + if (!success) { + logger.warn("Log stream buffer full, data may be delayed"); + } + } + } catch (error) { + logger.error( + `Failed to write to log stream: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + activeStreams.delete(stream); + if (!stream.destroyed) { + stream.destroy(); + } + } + } +} diff --git a/src/handlers/sockets.ts b/src/handlers/sockets.ts new file mode 100644 index 00000000..d176ea5f --- /dev/null +++ b/src/handlers/sockets.ts @@ -0,0 +1,9 @@ +import { createDockerStatsStream } from "./modules/docker-socket"; +import { createStackStream } from "./modules/live-stacks"; +import { createLogStream } from "./modules/logs-socket"; + +export const Sockets = { + createDockerStatsStream, + createLogStream, + createStackStream, +}; diff --git a/src/handlers/utils.ts b/src/handlers/utils.ts new file mode 100644 index 00000000..8d75f7be --- /dev/null +++ b/src/handlers/utils.ts @@ -0,0 +1,3 @@ +export async function CheckHealth() { + return "healthy"; +} diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts deleted file mode 100644 index 3a730229..00000000 --- a/src/middleware/auth.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { dbFunctions } from "~/core/database"; -import { logger } from "~/core/utils/logger"; - -import type { config } from "~/typings/database"; -import type { set } from "~/typings/elysiajs"; - -export async function hashApiKey(apiKey: string): Promise { - logger.debug("Hashing API key"); - try { - logger.debug("API key hashed successfully"); - return await Bun.password.hash(apiKey); - } catch (error) { - logger.error("Error hashing API key", error); - throw new Error("Failed to hash API key"); - } -} - -async function validateApiKeyHash( - providedKey: string, - storedHash: string, -): Promise { - logger.debug("Validating API key hash"); - try { - const isValid = await Bun.password.verify(providedKey, storedHash); - logger.debug(`API key validation result: ${isValid}`); - return isValid; - } catch (error) { - logger.error("Error validating API key hash", error); - return false; - } -} - -async function getApiKeyFromDb( - apiKey: string, -): Promise<{ hash: string } | null> { - const dbApiKey = (dbFunctions.getConfig() as config[])[0].api_key; - logger.debug(`Querying database for API key: ${apiKey}`); - return Promise.resolve({ - hash: dbApiKey, - }); -} - -export async function validateApiKey(request: Request, set: set) { - const apiKey = request.headers.get("x-api-key"); - - if (process.env.NODE_ENV !== "production") { - logger.warn( - "API Key validation deactivated, since running in development mode", - ); - return { success: true, apiKey }; - } - - if (!apiKey) { - logger.error(`API key missing from request ${request.url}`); - set.status = 401; - return { error: "API key required", success: false, apiKey }; - } - - logger.debug("API key validation initiated"); - - try { - const dbRecord = await getApiKeyFromDb(apiKey); - - if (!dbRecord) { - logger.error("API key not found in database"); - set.status = 401; - return { success: false, error: "Invalid API key" }; - } - - if (dbRecord.hash === "changeme") { - logger.error("Please change your API Key!"); - return { success: true, apiKey }; - } - - const isValid = await validateApiKeyHash(apiKey, dbRecord.hash); - - if (!isValid) { - logger.error("Invalid API key provided"); - set.status = 401; - return { success: false, error: "Invalid API key", apiKey }; - } - - logger.info("Valid API key used"); - } catch (error) { - logger.error("Error during API key validation", error); - set.status = 500; - return { success: false, error: "Internal server error", apiKey }; - } -} diff --git a/src/routes/docker-websocket.ts b/src/routes/docker-websocket.ts deleted file mode 100644 index 83d31c99..00000000 --- a/src/routes/docker-websocket.ts +++ /dev/null @@ -1,136 +0,0 @@ -import type { Readable } from "node:stream"; -import { Elysia } from "elysia"; -import type { ElysiaWS } from "elysia/dist/ws"; -import split2 from "split2"; - -import { dbFunctions } from "~/core/database"; -import { getDockerClient } from "~/core/docker/client"; -import { - calculateCpuPercent, - calculateMemoryUsage, -} from "~/core/utils/calculations"; -import { logger } from "~/core/utils/logger"; -import { responseHandler } from "~/core/utils/response-handler"; - -//biome-ignore lint/suspicious/noExplicitAny: -const activeDockerConnections = new Set>(); -const connectionStreams = new Map< - //biome-ignore lint/suspicious/noExplicitAny: - ElysiaWS, - Array<{ statsStream: Readable; splitStream: ReturnType }> ->(); - -export const dockerWebsocketRoutes = new Elysia({ prefix: "/ws" }).ws( - "/docker", - { - async open(ws) { - activeDockerConnections.add(ws); - connectionStreams.set(ws, []); - - ws.send(JSON.stringify({ message: "Connection established" })); - logger.info(`New Docker WebSocket established (${ws.id})`); - - try { - const hosts = dbFunctions.getDockerHosts(); - logger.debug(`Retrieved ${hosts.length} docker host(s)`); - - for (const host of hosts) { - if (ws.readyState !== 1) { - break; - } - - const docker = getDockerClient(host); - await docker.ping(); - const containers = await docker.listContainers({ all: true }); - logger.debug( - `Found ${containers.length} containers on ${host.name} (id: ${host.id})`, - ); - - for (const containerInfo of containers) { - if (ws.readyState !== 1) { - break; - } - - const container = docker.getContainer(containerInfo.Id); - const statsStream = (await container.stats({ - stream: true, - })) as Readable; - const splitStream = split2(); - - connectionStreams.get(ws)?.push({ statsStream, splitStream }); - - statsStream - .on("close", () => splitStream.destroy()) - .pipe(splitStream) - .on("data", (line: string) => { - if (ws.readyState !== 1 || !line) { - return; - } - try { - const stats = JSON.parse(line); - ws.send( - JSON.stringify({ - id: containerInfo.Id, - hostId: host.id, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats) || 0, - memoryUsage: calculateMemoryUsage(stats) || 0, - }), - ); - } catch (error) { - logger.error(`Parse error: ${error}`); - } - }) - .on("error", (error: Error) => { - logger.error(`Stream error: ${error}`); - statsStream.destroy(); - ws.send( - JSON.stringify({ - hostId: host.name, - containerId: containerInfo.Id, - error: `Stats stream error: ${error}`, - }), - ); - }); - } - } - } catch (error) { - logger.error(`Connection error: ${error}`); - ws.send( - JSON.stringify( - responseHandler.error( - { headers: {} }, - error as string, - "Docker connection failed", - 500, - ), - ), - ); - } - }, - - message(ws, message) { - if (message === "pong") ws.pong(); - }, - - close(ws) { - logger.info(`Closing connection ${ws.id}`); - activeDockerConnections.delete(ws); - - const streams = connectionStreams.get(ws) || []; - for (const { statsStream, splitStream } of streams) { - try { - statsStream.unpipe(splitStream); - statsStream.destroy(); - splitStream.destroy(); - } catch (error) { - logger.error(`Cleanup error: ${error}`); - } - } - connectionStreams.delete(ws); - }, - }, -); diff --git a/src/routes/live-logs.ts b/src/routes/live-logs.ts deleted file mode 100644 index 1b7fbfd8..00000000 --- a/src/routes/live-logs.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Elysia } from "elysia"; -import type { ElysiaWS } from "elysia/dist/ws"; - -import { logger } from "~/core/utils/logger"; - -import type { log_message } from "~/typings/database"; - -//biome-ignore lint/suspicious/noExplicitAny: -const activeConnections = new Set>(); - -export const liveLogs = new Elysia({ prefix: "/ws" }).ws("/logs", { - open(ws) { - activeConnections.add(ws); - ws.send({ - message: "Connection established", - level: "info", - timestamp: new Date().toISOString(), - file: "live-logs.ts", - line: 14, - }); - logger.info(`New Logs WebSocket established (${ws.id})`); - }, - close(ws) { - logger.info(`Logs WebSocket closed (${ws.id})`); - activeConnections.delete(ws); - }, -}); - -export function logToClients(data: log_message) { - for (const ws of activeConnections) { - try { - ws.send(JSON.stringify(data)); - } catch (error) { - activeConnections.delete(ws); - logger.error("Failed to send to WebSocket:", error); - } - } -} diff --git a/src/routes/live-stacks.ts b/src/routes/live-stacks.ts deleted file mode 100644 index b093fd21..00000000 --- a/src/routes/live-stacks.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Elysia } from "elysia"; -import type { ElysiaWS } from "elysia/dist/ws"; -import { logger } from "~/core/utils/logger"; -import type { stackSocketMessage } from "~/typings/websocket"; - -//biome-ignore lint/suspicious/noExplicitAny: Any = Connections -const activeConnections = new Set>(); - -export const liveStacks = new Elysia({ prefix: "/ws" }).ws("/stacks", { - open(ws) { - activeConnections.add(ws); - ws.send({ message: "Connection established" }); - logger.info(`New Stacks WebSocket established (${ws.id})`); - }, - close(ws) { - logger.info(`Stacks WebSocket closed (${ws.id})`); - activeConnections.delete(ws); - }, -}); - -export function postToClient(data: stackSocketMessage) { - for (const ws of activeConnections) { - try { - ws.send(JSON.stringify(data)); - } catch (error) { - activeConnections.delete(ws); - logger.error("Failed to send to WebSocket:", error); - } - } -} From dc0b939ede4a6b8fe452d091e776487a5e14b59f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sun, 29 Jun 2025 22:53:54 +0000 Subject: [PATCH 339/369] CQL: Apply lint fixes [skip ci] --- src/core/docker/scheduler.ts | 204 ++++++++++----------- src/handlers/config.ts | 340 +++++++++++++++++------------------ src/handlers/index.ts | 16 +- 3 files changed, 280 insertions(+), 280 deletions(-) diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index 480fe0f4..112187b4 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -5,119 +5,119 @@ import { logger } from "~/core/utils/logger"; import type { config } from "~/typings/database"; function convertFromMinToMs(minutes: number): number { - return minutes * 60 * 1000; + return minutes * 60 * 1000; } async function initialRun( - scheduleName: string, - scheduleFunction: Promise | void, - isAsync: boolean + scheduleName: string, + scheduleFunction: Promise | void, + isAsync: boolean, ) { - try { - if (isAsync) { - await scheduleFunction; - } else { - scheduleFunction; - } - logger.info(`Startup run success for: ${scheduleName}`); - } catch (error) { - logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); - } + try { + if (isAsync) { + await scheduleFunction; + } else { + scheduleFunction; + } + logger.info(`Startup run success for: ${scheduleName}`); + } catch (error) { + logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); + } } async function scheduledJob( - name: string, - jobFn: () => Promise, - intervalMs: number + name: string, + jobFn: () => Promise, + intervalMs: number, ) { - while (true) { - const start = Date.now(); - logger.info(`Task Start: ${name}`); - try { - await jobFn(); - logger.info(`Task End: ${name} succeeded.`); - } catch (e) { - logger.error(`Task End: ${name} failed:`, e); - } - const elapsed = Date.now() - start; - const delay = Math.max(0, intervalMs - elapsed); - await new Promise((r) => setTimeout(r, delay)); - } + while (true) { + const start = Date.now(); + logger.info(`Task Start: ${name}`); + try { + await jobFn(); + logger.info(`Task End: ${name} succeeded.`); + } catch (e) { + logger.error(`Task End: ${name} failed:`, e); + } + const elapsed = Date.now() - start; + const delay = Math.max(0, intervalMs - elapsed); + await new Promise((r) => setTimeout(r, delay)); + } } async function setSchedules() { - try { - const rawConfigData: unknown[] = dbFunctions.getConfig(); - const configData = rawConfigData[0]; - - if ( - !configData || - typeof (configData as config).keep_data_for !== "number" || - typeof (configData as config).fetching_interval !== "number" - ) { - logger.error("Invalid configuration data:", configData); - throw new Error("Invalid configuration data"); - } - - const { keep_data_for, fetching_interval } = configData as config; - - if (keep_data_for === undefined) { - const errMsg = "keep_data_for is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } - - if (fetching_interval === undefined) { - const errMsg = "fetching_interval is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } - - logger.info( - `Scheduling: Fetching container statistics every ${fetching_interval} minutes` - ); - - logger.info( - `Scheduling: Updating host statistics every ${fetching_interval} minutes` - ); - - logger.info( - `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days` - ); - - // Schedule container data fetching - await initialRun("storeContainerData", storeContainerData(), true); - scheduledJob( - "storeContainerData", - storeContainerData, - convertFromMinToMs(fetching_interval) - ); - - // Schedule Host statistics updates - await initialRun("storeHostData", storeHostData(), true); - scheduledJob( - "storeHostData", - storeHostData, - convertFromMinToMs(fetching_interval) - ); - - // Schedule database cleanup - await initialRun( - "dbFunctions.deleteOldData", - dbFunctions.deleteOldData(keep_data_for), - false - ); - scheduledJob( - "cleanupOldData", - () => Promise.resolve(dbFunctions.deleteOldData(keep_data_for)), - convertFromMinToMs(60) - ); - - logger.info("Schedules have been set successfully."); - } catch (error) { - logger.error("Error setting schedules:", error); - throw error; - } + try { + const rawConfigData: unknown[] = dbFunctions.getConfig(); + const configData = rawConfigData[0]; + + if ( + !configData || + typeof (configData as config).keep_data_for !== "number" || + typeof (configData as config).fetching_interval !== "number" + ) { + logger.error("Invalid configuration data:", configData); + throw new Error("Invalid configuration data"); + } + + const { keep_data_for, fetching_interval } = configData as config; + + if (keep_data_for === undefined) { + const errMsg = "keep_data_for is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + if (fetching_interval === undefined) { + const errMsg = "fetching_interval is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + logger.info( + `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, + ); + + logger.info( + `Scheduling: Updating host statistics every ${fetching_interval} minutes`, + ); + + logger.info( + `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, + ); + + // Schedule container data fetching + await initialRun("storeContainerData", storeContainerData(), true); + scheduledJob( + "storeContainerData", + storeContainerData, + convertFromMinToMs(fetching_interval), + ); + + // Schedule Host statistics updates + await initialRun("storeHostData", storeHostData(), true); + scheduledJob( + "storeHostData", + storeHostData, + convertFromMinToMs(fetching_interval), + ); + + // Schedule database cleanup + await initialRun( + "dbFunctions.deleteOldData", + dbFunctions.deleteOldData(keep_data_for), + false, + ); + scheduledJob( + "cleanupOldData", + () => Promise.resolve(dbFunctions.deleteOldData(keep_data_for)), + convertFromMinToMs(60), + ); + + logger.info("Schedules have been set successfully."); + } catch (error) { + logger.error("Error setting schedules:", error); + throw error; + } } export { setSchedules }; diff --git a/src/handlers/config.ts b/src/handlers/config.ts index 1057da7f..e02a49dd 100644 --- a/src/handlers/config.ts +++ b/src/handlers/config.ts @@ -4,181 +4,181 @@ import { backupDir } from "~/core/database/backup"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import type { config } from "~/typings/database"; import type { DockerHost } from "~/typings/docker"; class apiHandler { - async getConfig() { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateConfig(fetching_interval: number, keep_data_for: number) { - try { - dbFunctions.updateConfig(fetching_interval, keep_data_for); - return "Updated DockStatAPI config"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getPlugins() { - try { - return pluginManager.getPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getPackage() { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json` - ); - - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } - - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async createbackup() { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return backupFilename; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async listBackups() { - try { - const backupFiles = readdirSync(backupDir); - - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.startsWith(".") || - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); - - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async downloadbackup(downloadFile?: string) { - try { - const filename: string = downloadFile || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; - - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } - - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async restoreBackup(file: Bun.FileBlob) { - try { - if (!file) { - throw new Error("No file uploaded"); - } - - if (!(file.name || "").endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } - - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); - - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); - - return "Database restored successfully"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async addHost(host: DockerHost) { - try { - dbFunctions.addDockerHost(host); - return `Added docker host (${host.name} - ${host.hostAddress})`; - } catch (error: unknown) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateHost(host: DockerHost) { - try { - dbFunctions.updateDockerHost(host); - return `Updated docker host (${host.id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async removeHost(id: number) { - try { - dbFunctions.deleteDockerHost(id); - return `Deleted docker host (${id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } + async getConfig() { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateConfig(fetching_interval: number, keep_data_for: number) { + try { + dbFunctions.updateConfig(fetching_interval, keep_data_for); + return "Updated DockStatAPI config"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getPlugins() { + try { + return pluginManager.getPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getPackage() { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, + ); + + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } + + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async createbackup() { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return backupFilename; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async listBackups() { + try { + const backupFiles = readdirSync(backupDir); + + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.startsWith(".") || + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); + + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async downloadbackup(downloadFile?: string) { + try { + const filename: string = downloadFile || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; + + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } + + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async restoreBackup(file: Bun.FileBlob) { + try { + if (!file) { + throw new Error("No file uploaded"); + } + + if (!(file.name || "").endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } + + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return "Database restored successfully"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async addHost(host: DockerHost) { + try { + dbFunctions.addDockerHost(host); + return `Added docker host (${host.name} - ${host.hostAddress})`; + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateHost(host: DockerHost) { + try { + dbFunctions.updateDockerHost(host); + return `Updated docker host (${host.id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async removeHost(id: number) { + try { + dbFunctions.deleteDockerHost(id); + return `Deleted docker host (${id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } } export const ApiHandler = new apiHandler(); diff --git a/src/handlers/index.ts b/src/handlers/index.ts index 43fc11ba..b65a5d20 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -8,12 +8,12 @@ import { StackHandler } from "./stacks"; import { CheckHealth } from "./utils"; export const handlers = { - BasicDockerHandler, - ApiHandler, - DatabaseHandler, - StackHandler, - LogHandler, - CheckHealth, - sockets: Sockets, - start: setSchedules, + BasicDockerHandler, + ApiHandler, + DatabaseHandler, + StackHandler, + LogHandler, + CheckHealth, + sockets: Sockets, + start: setSchedules, }; From 5703198d9859bcbb6c5a55dff950d8ccde80d5b6 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Mon, 30 Jun 2025 02:56:50 +0200 Subject: [PATCH 340/369] feat(core): Move typings to root directory This commit moves the `typings` directory to the root of the project. This change affects the import paths in `runStackCommand.ts` and `logger.ts`, which have been updated accordingly. The diff includes: - Update import path for `postToClient` in `runStackCommand.ts` to `~/handlers/modules/live-stacks`. - Update import path for `Stack` in `runStackCommand.ts` to `~/../typings/docker-compose`. - Update import path for `log_message` in `logger.ts` to `../../../typings/database`. - Moves the typings dir to the root of the project The reason for this change is to simplify the project structure and make the typings more accessible to other parts of the application. --- bun.lock | 444 ------------------ src/core/stacks/operations/runStackCommand.ts | 146 +++--- src/core/utils/logger.ts | 334 ++++++------- typings | 1 + 4 files changed, 241 insertions(+), 684 deletions(-) delete mode 100644 bun.lock create mode 160000 typings diff --git a/bun.lock b/bun.lock deleted file mode 100644 index f2cc38f1..00000000 --- a/bun.lock +++ /dev/null @@ -1,444 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "dockstatapi", - "dependencies": { - "@elysiajs/server-timing": "^1.3.0", - "@elysiajs/static": "^1.3.0", - "@elysiajs/swagger": "^1.3.0", - "chalk": "^5.4.1", - "date-fns": "^4.1.0", - "docker-compose": "^1.2.0", - "dockerode": "^4.0.6", - "elysia": "latest", - "elysia-remote-dts": "^1.0.2", - "knip": "latest", - "logestic": "^1.2.4", - "split2": "^4.2.0", - "winston": "^3.17.0", - "yaml": "^2.7.1", - }, - "devDependencies": { - "@biomejs/biome": "1.9.4", - "@types/bun": "latest", - "@types/dockerode": "^3.3.38", - "@types/node": "^22.15.17", - "@types/split2": "^4.2.3", - "bun-types": "latest", - "cross-env": "^7.0.3", - "logform": "^2.7.0", - "typescript": "^5.8.3", - "wrap-ansi": "^9.0.0", - }, - }, - }, - "trustedDependencies": [ - "protobufjs", - ], - "packages": { - "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], - - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], - - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], - - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], - - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], - - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], - - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], - - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], - - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], - - "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], - - "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], - - "@elysiajs/server-timing": ["@elysiajs/server-timing@1.3.0", "", { "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-c5Ay0Va7gIWjJ9CawHx05UtKP6UQVkMKCFnf16eBG0G/GgUkrMMGHWD/duCBaDbeRwbbb7IwHDoaFvStWrB2IQ=="], - - "@elysiajs/static": ["@elysiajs/static@1.3.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-7mWlj2U/AZvH27IfRKqpUjDP1W9ZRldF9NmdnatFEtx0AOy7YYgyk0rt5hXrH6wPcR//2gO2Qy+k5rwswpEhJA=="], - - "@elysiajs/swagger": ["@elysiajs/swagger@1.3.0", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-0fo3FWkDRPNYpowJvLz3jBHe9bFe6gruZUyf+feKvUEEMG9ZHptO1jolSoPE0ffFw1BgN1/wMsP19p4GRXKdfg=="], - - "@grpc/grpc-js": ["@grpc/grpc-js@1.13.3", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg=="], - - "@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="], - - "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], - - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], - - "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], - - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], - - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], - - "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - - "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], - - "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], - - "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], - - "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], - - "@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="], - - "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], - - "@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="], - - "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], - - "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - - "@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="], - - "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], - - "@types/dockerode": ["@types/dockerode@3.3.38", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-nnrcfUe2iR+RyOuz0B4bZgQwD9djQa9ADEjp7OAgBs10pYT0KSCtplJjcmBDJz0qaReX5T7GbE5i4VplvzUHvA=="], - - "@types/node": ["@types/node@22.15.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw=="], - - "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], - - "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], - - "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], - - "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], - - "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - - "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], - - "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], - - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - - "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], - - "bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="], - - "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], - - "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], - - "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - - "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], - - "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], - - "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], - - "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], - - "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - - "docker-compose": ["docker-compose@1.2.0", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-wIU1eHk3Op7dFgELRdmOYlPYS4gP8HhH1ZmZa13QZF59y0fblzFDFmKPhyc05phCy2hze9OEvNZAsoljrs+72w=="], - - "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], - - "dockerode": ["dockerode@4.0.6", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.1.2", "uuid": "^10.0.0" } }, "sha512-FbVf3Z8fY/kALB9s+P9epCpWhfi/r0N2DgYYcYpsAUlaTxPjdsitsFobnltb+lyCgAIvf9C+4PSWlTnHlJMf1w=="], - - "elysia": ["elysia@1.3.1", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-En41P6cDHcHtQ0nvfsn9ayB+8ahQJqG1nzvPX8FVZjOriFK/RtZPQBtXMfZDq/AsVIk7JFZGFEtAVEmztNJVhQ=="], - - "elysia-remote-dts": ["elysia-remote-dts@1.0.2", "", { "dependencies": { "debug": "4.4.0", "get-tsconfig": "4.10.0" }, "peerDependencies": { "elysia": ">= 1.0.0", "typescript": ">=5" } }, "sha512-ktRxKGozPDW24d3xbUS2sMLNsRHHX/a4Pgqyzv2O0X4HsDrD+agoUYL/PvYQrGJKPSc3xzvU5uvhNHFhEql6aw=="], - - "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], - - "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], - - "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], - - "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="], - - "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], - - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], - - "fd-package-json": ["fd-package-json@1.2.0", "", { "dependencies": { "walk-up-path": "^3.0.1" } }, "sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA=="], - - "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], - - "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], - - "file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], - - "formatly": ["formatly@0.2.3", "", { "dependencies": { "fd-package-json": "^1.2.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-WH01vbXEjh9L3bqn5V620xUAWs32CmK4IzWRRY6ep5zpa/mrisL4d9+pRVuETORVDTQw8OycSO1WC68PL51RaA=="], - - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], - - "get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="], - - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], - - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], - - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - - "knip": ["knip@5.55.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "enhanced-resolve": "^5.18.1", "fast-glob": "^3.3.3", "formatly": "^0.2.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-NYXjgGrXgMdabUKCP2TlBH/e83m9KnLc1VLyWHUtoRrCEJ/C15YtbafrpTvm3td+jE4VdDPgudvXT1IMtCx8lw=="], - - "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], - - "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], - - "logestic": ["logestic@1.2.4", "", { "dependencies": { "chalk": "^5.3.0" }, "peerDependencies": { "elysia": "^1.1.3", "typescript": "^5.0.0" } }, "sha512-Wka/xFdKgqU6JBk8yxAUsqcUjPA/aExpcnm7KnOAxlLo1U71kuWGeEjPw8XVLZzLleTWwmRqJUb2yI5XZP+vAA=="], - - "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], - - "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="], - - "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], - - "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], - - "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], - - "peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "protobufjs": ["protobufjs@7.5.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3qx3IRjR9WPQKagdwrKjO3Gu8RgQR2qqw+1KnigWhoVjFqegIj1K3bP11sGqhxrO46/XL7lekuG4jmjL+4cLsw=="], - - "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], - - "smol-toml": ["smol-toml@1.3.4", "", {}, "sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA=="], - - "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], - - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - - "ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="], - - "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], - - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - - "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="], - - "strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="], - - "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], - - "tar-fs": ["tar-fs@2.1.2", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA=="], - - "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - - "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="], - - "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], - - "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], - - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - - "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - - "walk-up-path": ["walk-up-path@3.0.1", "", {}, "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], - - "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], - - "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - - "yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], - - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], - - "zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="], - - "zod-validation-error": ["zod-validation-error@3.4.1", "", { "peerDependencies": { "zod": "^3.24.4" } }, "sha512-1KP64yqDPQ3rupxNv7oXhf7KdhHHgaqbKuspVoiN93TT0xrBjql+Svjkdjq/Qh/7GSMmgQs3AfvBT0heE35thw=="], - - "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], - - "@types/ssh2/@types/node": ["@types/node@18.19.100", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-ojmMP8SZBKprc3qGrGk8Ujpo80AXkrP7G2tOT4VWr5jlr5DHjsJF+emXJz+Wm0glmy4Js62oKMdZZ6B9Y+tEcA=="], - - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], - - "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - } -} diff --git a/src/core/stacks/operations/runStackCommand.ts b/src/core/stacks/operations/runStackCommand.ts index 818e6499..bfc36a1e 100644 --- a/src/core/stacks/operations/runStackCommand.ts +++ b/src/core/stacks/operations/runStackCommand.ts @@ -1,89 +1,89 @@ import { logger } from "~/core/utils/logger"; -import { postToClient } from "~/routes/live-stacks"; -import type { Stack } from "~/typings/docker-compose"; +import { postToClient } from "~/handlers/modules/live-stacks"; +import type { Stack } from "~/../typings/docker-compose"; import { getStackName, getStackPath } from "./stackHelpers"; export function wrapProgressCallback(progressCallback?: (log: string) => void) { - return progressCallback - ? (chunk: Buffer) => { - const log = chunk.toString(); - progressCallback(log); - } - : undefined; + return progressCallback + ? (chunk: Buffer) => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; } export async function runStackCommand( - stack_id: number, - command: ( - cwd: string, - progressCallback?: (log: string) => void, - ) => Promise, - action: string, + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void + ) => Promise, + action: string ): Promise { - try { - logger.debug( - `Starting runStackCommand for stack_id=${stack_id}, action="${action}"`, - ); + try { + logger.debug( + `Starting runStackCommand for stack_id=${stack_id}, action="${action}"` + ); - const stackName = await getStackName(stack_id); - logger.debug( - `Retrieved stack name "${stackName}" for stack_id=${stack_id}`, - ); + const stackName = await getStackName(stack_id); + logger.debug( + `Retrieved stack name "${stackName}" for stack_id=${stack_id}` + ); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); - const progressCallback = (log: string) => { - const message = log.trim(); - logger.debug( - `Progress for stack_id=${stack_id}, action="${action}": ${message}`, - ); + const progressCallback = (log: string) => { + const message = log.trim(); + logger.debug( + `Progress for stack_id=${stack_id}, action="${action}": ${message}` + ); - if (message.includes("Error response from daemon")) { - const extracted = message.match(/Error response from daemon: (.+)/); - if (extracted) { - logger.error(`Error response from daemon: ${extracted[1]}`); - } - } + if (message.includes("Error response from daemon")) { + const extracted = message.match(/Error response from daemon: (.+)/); + if (extracted) { + logger.error(`Error response from daemon: ${extracted[1]}`); + } + } - postToClient({ - type: "stack-progress", - data: { - stack_id, - action, - message, - timestamp: new Date().toISOString(), - }, - }); - }; + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message, + timestamp: new Date().toISOString(), + }, + }); + }; - logger.debug( - `Executing command for stack_id=${stack_id}, action="${action}"`, - ); - const result = await command(stackPath, progressCallback); - logger.debug( - `Successfully completed command for stack_id=${stack_id}, action="${action}"`, - ); + logger.debug( + `Executing command for stack_id=${stack_id}, action="${action}"` + ); + const result = await command(stackPath, progressCallback); + logger.debug( + `Successfully completed command for stack_id=${stack_id}, action="${action}"` + ); - return result; - } catch (error: unknown) { - const errorMsg = - error instanceof Error ? error.message : JSON.stringify(error); - logger.debug( - `Error occurred for stack_id=${stack_id}, action="${action}": ${errorMsg}`, - ); - postToClient({ - type: "stack-error", - data: { - stack_id, - action, - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(`Error while ${action} stack "${stack_id}": ${errorMsg}`); - } + return result; + } catch (error: unknown) { + const errorMsg = + error instanceof Error ? error.message : JSON.stringify(error); + logger.debug( + `Error occurred for stack_id=${stack_id}, action="${action}": ${errorMsg}` + ); + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(`Error while ${action} stack "${stack_id}": ${errorMsg}`); + } } diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 1f16f809..438b34bc 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -8,195 +8,195 @@ import { dbFunctions } from "~/core/database"; import { logToClients } from "~/handlers/modules/logs-socket"; -import type { log_message } from "~/typings/database"; +import type { log_message } from "../../../typings/database"; import { backupInProgress } from "../database/_dbState"; const padNewlines = process.env.PAD_NEW_LINES !== "false"; type LogLevel = - | "error" - | "warn" - | "info" - | "debug" - | "verbose" - | "silly" - | "task" - | "ut"; - -// biome-ignore lint/suspicious/noControlCharactersInRegex: + | "error" + | "warn" + | "info" + | "debug" + | "verbose" + | "silly" + | "task" + | "ut"; + +// biome-ignore lint/suspicious/noControlCharactersInRegex: const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; const formatTerminalMessage = (message: string, prefix: string): string => { - try { - const cleanPrefix = prefix.replace(ansiRegex, ""); - const maxWidth = process.stdout.columns || 80; - const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); - - if (!padNewlines) return message; - - const wrapped = wrapAnsi(message, wrapWidth, { - trim: true, - hard: true, - wordWrap: true, - }); - - return wrapped - .split("\n") - .map((line, index) => { - return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; - }) - .join("\n"); - } catch (error) { - console.error("Error formatting terminal message:", error); - return message; - } + try { + const cleanPrefix = prefix.replace(ansiRegex, ""); + const maxWidth = process.stdout.columns || 80; + const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); + + if (!padNewlines) return message; + + const wrapped = wrapAnsi(message, wrapWidth, { + trim: true, + hard: true, + wordWrap: true, + }); + + return wrapped + .split("\n") + .map((line, index) => { + return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; + }) + .join("\n"); + } catch (error) { + console.error("Error formatting terminal message:", error); + return message; + } }; const levelColors: Record = { - error: chalk.red.bold, - warn: chalk.yellow.bold, - info: chalk.green.bold, - debug: chalk.blue.bold, - verbose: chalk.cyan.bold, - silly: chalk.magenta.bold, - task: chalk.cyan.bold, - ut: chalk.hex("#9D00FF"), + error: chalk.red.bold, + warn: chalk.yellow.bold, + info: chalk.green.bold, + debug: chalk.blue.bold, + verbose: chalk.cyan.bold, + silly: chalk.magenta.bold, + task: chalk.cyan.bold, + ut: chalk.hex("#9D00FF"), }; const parseTimestamp = (timestamp: string): string => { - const [datePart, timePart] = timestamp.split(" "); - const [day, month] = datePart.split("/"); - const [hours, minutes, seconds] = timePart.split(":"); - const year = new Date().getFullYear(); - const date = new Date( - year, - Number.parseInt(month) - 1, - Number.parseInt(day), - Number.parseInt(hours), - Number.parseInt(minutes), - Number.parseInt(seconds), - ); - return date.toISOString(); + const [datePart, timePart] = timestamp.split(" "); + const [day, month] = datePart.split("/"); + const [hours, minutes, seconds] = timePart.split(":"); + const year = new Date().getFullYear(); + const date = new Date( + year, + Number.parseInt(month) - 1, + Number.parseInt(day), + Number.parseInt(hours), + Number.parseInt(minutes), + Number.parseInt(seconds) + ); + return date.toISOString(); }; const handleWebSocketLog = (log: log_message) => { - try { - logToClients({ - ...log, - timestamp: parseTimestamp(log.timestamp), - }); - } catch (error) { - console.error( - `WebSocket logging failed: ${ - error instanceof Error ? error.message : error - }`, - ); - } + try { + logToClients({ + ...log, + timestamp: parseTimestamp(log.timestamp), + }); + } catch (error) { + console.error( + `WebSocket logging failed: ${ + error instanceof Error ? error.message : error + }` + ); + } }; const handleDatabaseLog = (log: log_message): void => { - if (backupInProgress) { - return; - } - try { - dbFunctions.addLogEntry({ - ...log, - timestamp: parseTimestamp(log.timestamp), - }); - } catch (error) { - console.error( - `Database logging failed: ${ - error instanceof Error ? error.message : error - }`, - ); - } + if (backupInProgress) { + return; + } + try { + dbFunctions.addLogEntry({ + ...log, + timestamp: parseTimestamp(log.timestamp), + }); + } catch (error) { + console.error( + `Database logging failed: ${ + error instanceof Error ? error.message : error + }` + ); + } }; export const logger = createLogger({ - level: process.env.LOG_LEVEL || "debug", - format: format.combine( - format.timestamp({ format: "DD/MM HH:mm:ss" }), - format((info) => { - const stack = new Error().stack?.split("\n"); - let file = "unknown"; - let line = 0; - - if (stack) { - for (let i = 2; i < stack.length; i++) { - const lineStr = stack[i].trim(); - if ( - !lineStr.includes("node_modules") && - !lineStr.includes(path.basename(__filename)) - ) { - const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); - if (matches) { - file = path.basename(matches[1]); - line = Number.parseInt(matches[2], 10); - break; - } - } - } - } - return { ...info, file, line }; - })(), - format.printf((info) => { - const { timestamp, level, message, file, line } = - info as TransformableInfo & log_message; - let processedLevel = level as LogLevel; - let processedMessage = String(message); - - if (processedMessage.startsWith("__task__")) { - processedMessage = processedMessage - .replace(/__task__/g, "") - .trimStart(); - processedLevel = "task"; - if (processedMessage.startsWith("__db__")) { - processedMessage = processedMessage - .replace(/__db__/g, "") - .trimStart(); - processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; - } - } else if (processedMessage.startsWith("__UT__")) { - processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); - processedLevel = "ut"; - } - - if (file.endsWith("plugin.ts")) { - processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; - } - - const paddedLevel = processedLevel.toUpperCase().padEnd(5); - const coloredLevel = (levelColors[processedLevel] || chalk.white)( - paddedLevel, - ); - const coloredContext = chalk.cyan(`${file}:${line}`); - const coloredTimestamp = chalk.yellow(timestamp); - - const prefix = `${paddedLevel} [ ${timestamp} ] - `; - const combinedContent = `${processedMessage} - ${coloredContext}`; - - const formattedMessage = padNewlines - ? formatTerminalMessage(combinedContent, prefix) - : combinedContent; - - handleDatabaseLog({ - level: processedLevel, - timestamp: timestamp, - message: processedMessage, - file: file, - line: line, - }); - handleWebSocketLog({ - level: processedLevel, - timestamp: timestamp, - message: processedMessage, - file: file, - line: line, - }); - - return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; - }), - ), - transports: [new transports.Console()], + level: process.env.LOG_LEVEL || "debug", + format: format.combine( + format.timestamp({ format: "DD/MM HH:mm:ss" }), + format((info) => { + const stack = new Error().stack?.split("\n"); + let file = "unknown"; + let line = 0; + + if (stack) { + for (let i = 2; i < stack.length; i++) { + const lineStr = stack[i].trim(); + if ( + !lineStr.includes("node_modules") && + !lineStr.includes(path.basename(import.meta.url)) + ) { + const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); + if (matches) { + file = path.basename(matches[1]); + line = Number.parseInt(matches[2], 10); + break; + } + } + } + } + return { ...info, file, line }; + })(), + format.printf((info) => { + const { timestamp, level, message, file, line } = + info as TransformableInfo & log_message; + let processedLevel = level as LogLevel; + let processedMessage = String(message); + + if (processedMessage.startsWith("__task__")) { + processedMessage = processedMessage + .replace(/__task__/g, "") + .trimStart(); + processedLevel = "task"; + if (processedMessage.startsWith("__db__")) { + processedMessage = processedMessage + .replace(/__db__/g, "") + .trimStart(); + processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; + } + } else if (processedMessage.startsWith("__UT__")) { + processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); + processedLevel = "ut"; + } + + if (file.endsWith("plugin.ts")) { + processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; + } + + const paddedLevel = processedLevel.toUpperCase().padEnd(5); + const coloredLevel = (levelColors[processedLevel] || chalk.white)( + paddedLevel + ); + const coloredContext = chalk.cyan(`${file}:${line}`); + const coloredTimestamp = chalk.yellow(timestamp); + + const prefix = `${paddedLevel} [ ${timestamp} ] - `; + const combinedContent = `${processedMessage} - ${coloredContext}`; + + const formattedMessage = padNewlines + ? formatTerminalMessage(combinedContent, prefix) + : combinedContent; + + handleDatabaseLog({ + level: processedLevel, + timestamp: timestamp, + message: processedMessage, + file: file, + line: line, + }); + handleWebSocketLog({ + level: processedLevel, + timestamp: timestamp, + message: processedMessage, + file: file, + line: line, + }); + + return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; + }) + ), + transports: [new transports.Console()], }); diff --git a/typings b/typings new file mode 160000 index 00000000..38ef6e0b --- /dev/null +++ b/typings @@ -0,0 +1 @@ +Subproject commit 38ef6e0bb047ff502e76e440b836b214ba1f04fe From 4e643d74d97eacb07670c16917d4f315ccb97423 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 30 Jun 2025 00:57:25 +0000 Subject: [PATCH 341/369] CQL: Apply lint fixes [skip ci] --- src/core/stacks/operations/runStackCommand.ts | 144 ++++---- src/core/utils/logger.ts | 328 +++++++++--------- 2 files changed, 236 insertions(+), 236 deletions(-) diff --git a/src/core/stacks/operations/runStackCommand.ts b/src/core/stacks/operations/runStackCommand.ts index bfc36a1e..f4fdf739 100644 --- a/src/core/stacks/operations/runStackCommand.ts +++ b/src/core/stacks/operations/runStackCommand.ts @@ -1,89 +1,89 @@ +import type { Stack } from "~/../typings/docker-compose"; import { logger } from "~/core/utils/logger"; import { postToClient } from "~/handlers/modules/live-stacks"; -import type { Stack } from "~/../typings/docker-compose"; import { getStackName, getStackPath } from "./stackHelpers"; export function wrapProgressCallback(progressCallback?: (log: string) => void) { - return progressCallback - ? (chunk: Buffer) => { - const log = chunk.toString(); - progressCallback(log); - } - : undefined; + return progressCallback + ? (chunk: Buffer) => { + const log = chunk.toString(); + progressCallback(log); + } + : undefined; } export async function runStackCommand( - stack_id: number, - command: ( - cwd: string, - progressCallback?: (log: string) => void - ) => Promise, - action: string + stack_id: number, + command: ( + cwd: string, + progressCallback?: (log: string) => void, + ) => Promise, + action: string, ): Promise { - try { - logger.debug( - `Starting runStackCommand for stack_id=${stack_id}, action="${action}"` - ); + try { + logger.debug( + `Starting runStackCommand for stack_id=${stack_id}, action="${action}"`, + ); - const stackName = await getStackName(stack_id); - logger.debug( - `Retrieved stack name "${stackName}" for stack_id=${stack_id}` - ); + const stackName = await getStackName(stack_id); + logger.debug( + `Retrieved stack name "${stackName}" for stack_id=${stack_id}`, + ); - const stackPath = await getStackPath({ - id: stack_id, - name: stackName, - } as Stack); - logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); + const stackPath = await getStackPath({ + id: stack_id, + name: stackName, + } as Stack); + logger.debug(`Resolved stack path "${stackPath}" for stack_id=${stack_id}`); - const progressCallback = (log: string) => { - const message = log.trim(); - logger.debug( - `Progress for stack_id=${stack_id}, action="${action}": ${message}` - ); + const progressCallback = (log: string) => { + const message = log.trim(); + logger.debug( + `Progress for stack_id=${stack_id}, action="${action}": ${message}`, + ); - if (message.includes("Error response from daemon")) { - const extracted = message.match(/Error response from daemon: (.+)/); - if (extracted) { - logger.error(`Error response from daemon: ${extracted[1]}`); - } - } + if (message.includes("Error response from daemon")) { + const extracted = message.match(/Error response from daemon: (.+)/); + if (extracted) { + logger.error(`Error response from daemon: ${extracted[1]}`); + } + } - postToClient({ - type: "stack-progress", - data: { - stack_id, - action, - message, - timestamp: new Date().toISOString(), - }, - }); - }; + postToClient({ + type: "stack-progress", + data: { + stack_id, + action, + message, + timestamp: new Date().toISOString(), + }, + }); + }; - logger.debug( - `Executing command for stack_id=${stack_id}, action="${action}"` - ); - const result = await command(stackPath, progressCallback); - logger.debug( - `Successfully completed command for stack_id=${stack_id}, action="${action}"` - ); + logger.debug( + `Executing command for stack_id=${stack_id}, action="${action}"`, + ); + const result = await command(stackPath, progressCallback); + logger.debug( + `Successfully completed command for stack_id=${stack_id}, action="${action}"`, + ); - return result; - } catch (error: unknown) { - const errorMsg = - error instanceof Error ? error.message : JSON.stringify(error); - logger.debug( - `Error occurred for stack_id=${stack_id}, action="${action}": ${errorMsg}` - ); - postToClient({ - type: "stack-error", - data: { - stack_id, - action, - message: errorMsg, - timestamp: new Date().toISOString(), - }, - }); - throw new Error(`Error while ${action} stack "${stack_id}": ${errorMsg}`); - } + return result; + } catch (error: unknown) { + const errorMsg = + error instanceof Error ? error.message : JSON.stringify(error); + logger.debug( + `Error occurred for stack_id=${stack_id}, action="${action}": ${errorMsg}`, + ); + postToClient({ + type: "stack-error", + data: { + stack_id, + action, + message: errorMsg, + timestamp: new Date().toISOString(), + }, + }); + throw new Error(`Error while ${action} stack "${stack_id}": ${errorMsg}`); + } } diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 438b34bc..b9f3863a 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -15,188 +15,188 @@ import { backupInProgress } from "../database/_dbState"; const padNewlines = process.env.PAD_NEW_LINES !== "false"; type LogLevel = - | "error" - | "warn" - | "info" - | "debug" - | "verbose" - | "silly" - | "task" - | "ut"; + | "error" + | "warn" + | "info" + | "debug" + | "verbose" + | "silly" + | "task" + | "ut"; // biome-ignore lint/suspicious/noControlCharactersInRegex: const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; const formatTerminalMessage = (message: string, prefix: string): string => { - try { - const cleanPrefix = prefix.replace(ansiRegex, ""); - const maxWidth = process.stdout.columns || 80; - const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); - - if (!padNewlines) return message; - - const wrapped = wrapAnsi(message, wrapWidth, { - trim: true, - hard: true, - wordWrap: true, - }); - - return wrapped - .split("\n") - .map((line, index) => { - return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; - }) - .join("\n"); - } catch (error) { - console.error("Error formatting terminal message:", error); - return message; - } + try { + const cleanPrefix = prefix.replace(ansiRegex, ""); + const maxWidth = process.stdout.columns || 80; + const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); + + if (!padNewlines) return message; + + const wrapped = wrapAnsi(message, wrapWidth, { + trim: true, + hard: true, + wordWrap: true, + }); + + return wrapped + .split("\n") + .map((line, index) => { + return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; + }) + .join("\n"); + } catch (error) { + console.error("Error formatting terminal message:", error); + return message; + } }; const levelColors: Record = { - error: chalk.red.bold, - warn: chalk.yellow.bold, - info: chalk.green.bold, - debug: chalk.blue.bold, - verbose: chalk.cyan.bold, - silly: chalk.magenta.bold, - task: chalk.cyan.bold, - ut: chalk.hex("#9D00FF"), + error: chalk.red.bold, + warn: chalk.yellow.bold, + info: chalk.green.bold, + debug: chalk.blue.bold, + verbose: chalk.cyan.bold, + silly: chalk.magenta.bold, + task: chalk.cyan.bold, + ut: chalk.hex("#9D00FF"), }; const parseTimestamp = (timestamp: string): string => { - const [datePart, timePart] = timestamp.split(" "); - const [day, month] = datePart.split("/"); - const [hours, minutes, seconds] = timePart.split(":"); - const year = new Date().getFullYear(); - const date = new Date( - year, - Number.parseInt(month) - 1, - Number.parseInt(day), - Number.parseInt(hours), - Number.parseInt(minutes), - Number.parseInt(seconds) - ); - return date.toISOString(); + const [datePart, timePart] = timestamp.split(" "); + const [day, month] = datePart.split("/"); + const [hours, minutes, seconds] = timePart.split(":"); + const year = new Date().getFullYear(); + const date = new Date( + year, + Number.parseInt(month) - 1, + Number.parseInt(day), + Number.parseInt(hours), + Number.parseInt(minutes), + Number.parseInt(seconds), + ); + return date.toISOString(); }; const handleWebSocketLog = (log: log_message) => { - try { - logToClients({ - ...log, - timestamp: parseTimestamp(log.timestamp), - }); - } catch (error) { - console.error( - `WebSocket logging failed: ${ - error instanceof Error ? error.message : error - }` - ); - } + try { + logToClients({ + ...log, + timestamp: parseTimestamp(log.timestamp), + }); + } catch (error) { + console.error( + `WebSocket logging failed: ${ + error instanceof Error ? error.message : error + }`, + ); + } }; const handleDatabaseLog = (log: log_message): void => { - if (backupInProgress) { - return; - } - try { - dbFunctions.addLogEntry({ - ...log, - timestamp: parseTimestamp(log.timestamp), - }); - } catch (error) { - console.error( - `Database logging failed: ${ - error instanceof Error ? error.message : error - }` - ); - } + if (backupInProgress) { + return; + } + try { + dbFunctions.addLogEntry({ + ...log, + timestamp: parseTimestamp(log.timestamp), + }); + } catch (error) { + console.error( + `Database logging failed: ${ + error instanceof Error ? error.message : error + }`, + ); + } }; export const logger = createLogger({ - level: process.env.LOG_LEVEL || "debug", - format: format.combine( - format.timestamp({ format: "DD/MM HH:mm:ss" }), - format((info) => { - const stack = new Error().stack?.split("\n"); - let file = "unknown"; - let line = 0; - - if (stack) { - for (let i = 2; i < stack.length; i++) { - const lineStr = stack[i].trim(); - if ( - !lineStr.includes("node_modules") && - !lineStr.includes(path.basename(import.meta.url)) - ) { - const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); - if (matches) { - file = path.basename(matches[1]); - line = Number.parseInt(matches[2], 10); - break; - } - } - } - } - return { ...info, file, line }; - })(), - format.printf((info) => { - const { timestamp, level, message, file, line } = - info as TransformableInfo & log_message; - let processedLevel = level as LogLevel; - let processedMessage = String(message); - - if (processedMessage.startsWith("__task__")) { - processedMessage = processedMessage - .replace(/__task__/g, "") - .trimStart(); - processedLevel = "task"; - if (processedMessage.startsWith("__db__")) { - processedMessage = processedMessage - .replace(/__db__/g, "") - .trimStart(); - processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; - } - } else if (processedMessage.startsWith("__UT__")) { - processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); - processedLevel = "ut"; - } - - if (file.endsWith("plugin.ts")) { - processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; - } - - const paddedLevel = processedLevel.toUpperCase().padEnd(5); - const coloredLevel = (levelColors[processedLevel] || chalk.white)( - paddedLevel - ); - const coloredContext = chalk.cyan(`${file}:${line}`); - const coloredTimestamp = chalk.yellow(timestamp); - - const prefix = `${paddedLevel} [ ${timestamp} ] - `; - const combinedContent = `${processedMessage} - ${coloredContext}`; - - const formattedMessage = padNewlines - ? formatTerminalMessage(combinedContent, prefix) - : combinedContent; - - handleDatabaseLog({ - level: processedLevel, - timestamp: timestamp, - message: processedMessage, - file: file, - line: line, - }); - handleWebSocketLog({ - level: processedLevel, - timestamp: timestamp, - message: processedMessage, - file: file, - line: line, - }); - - return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; - }) - ), - transports: [new transports.Console()], + level: process.env.LOG_LEVEL || "debug", + format: format.combine( + format.timestamp({ format: "DD/MM HH:mm:ss" }), + format((info) => { + const stack = new Error().stack?.split("\n"); + let file = "unknown"; + let line = 0; + + if (stack) { + for (let i = 2; i < stack.length; i++) { + const lineStr = stack[i].trim(); + if ( + !lineStr.includes("node_modules") && + !lineStr.includes(path.basename(import.meta.url)) + ) { + const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); + if (matches) { + file = path.basename(matches[1]); + line = Number.parseInt(matches[2], 10); + break; + } + } + } + } + return { ...info, file, line }; + })(), + format.printf((info) => { + const { timestamp, level, message, file, line } = + info as TransformableInfo & log_message; + let processedLevel = level as LogLevel; + let processedMessage = String(message); + + if (processedMessage.startsWith("__task__")) { + processedMessage = processedMessage + .replace(/__task__/g, "") + .trimStart(); + processedLevel = "task"; + if (processedMessage.startsWith("__db__")) { + processedMessage = processedMessage + .replace(/__db__/g, "") + .trimStart(); + processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; + } + } else if (processedMessage.startsWith("__UT__")) { + processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); + processedLevel = "ut"; + } + + if (file.endsWith("plugin.ts")) { + processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; + } + + const paddedLevel = processedLevel.toUpperCase().padEnd(5); + const coloredLevel = (levelColors[processedLevel] || chalk.white)( + paddedLevel, + ); + const coloredContext = chalk.cyan(`${file}:${line}`); + const coloredTimestamp = chalk.yellow(timestamp); + + const prefix = `${paddedLevel} [ ${timestamp} ] - `; + const combinedContent = `${processedMessage} - ${coloredContext}`; + + const formattedMessage = padNewlines + ? formatTerminalMessage(combinedContent, prefix) + : combinedContent; + + handleDatabaseLog({ + level: processedLevel, + timestamp: timestamp, + message: processedMessage, + file: file, + line: line, + }); + handleWebSocketLog({ + level: processedLevel, + timestamp: timestamp, + message: processedMessage, + file: file, + line: line, + }); + + return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; + }), + ), + transports: [new transports.Console()], }); From 10c04224eda5d7d49659601b8ea1db9591d2a1ef Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Mon, 30 Jun 2025 13:56:11 +0200 Subject: [PATCH 342/369] feat(handlers): Needs testing / fixing --- src/core/docker/client.ts | 54 ++-- src/core/docker/scheduler.ts | 207 ++++++++-------- src/core/plugins/plugin-manager.ts | 324 ++++++++++++------------ src/handlers/config.ts | 345 +++++++++++++------------- src/handlers/docker.ts | 296 +++++++++++----------- src/handlers/index.ts | 16 +- src/handlers/modules/docker-socket.ts | 239 +++++++++--------- src/handlers/stacks.ts | 234 ++++++++--------- src/handlers/utils.ts | 5 +- 9 files changed, 865 insertions(+), 855 deletions(-) diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index ad65540b..cd252d68 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -1,33 +1,35 @@ import Docker from "dockerode"; import { logger } from "~/core/utils/logger"; -import type { DockerHost } from "~/typings/docker"; +import type { DockerHost } from "../../../typings/docker"; export const getDockerClient = (host: DockerHost): Docker => { - try { - const inputUrl = host.hostAddress.includes("://") - ? host.hostAddress - : `${host.secure ? "https" : "http"}://${host.hostAddress}`; - const parsedUrl = new URL(inputUrl); - const hostAddress = parsedUrl.hostname; - const port = parsedUrl.port - ? Number.parseInt(parsedUrl.port) - : host.secure - ? 2376 - : 2375; + try { + logger.info(`Setting up host: ${JSON.stringify(host)}`); - if (Number.isNaN(port) || port < 1 || port > 65535) { - throw new Error("Invalid port number in Docker host URL"); - } + const inputUrl = host.hostAddress.includes("://") + ? host.hostAddress + : `${host.secure ? "https" : "http"}://${host.hostAddress}`; + const parsedUrl = new URL(inputUrl); + const hostAddress = parsedUrl.hostname; + const port = parsedUrl.port + ? Number.parseInt(parsedUrl.port) + : host.secure + ? 2376 + : 2375; - return new Docker({ - protocol: host.secure ? "https" : "http", - host: hostAddress, - port, - version: "v1.41", - // TODO: Add TLS configuration if needed - }); - } catch (error) { - logger.error("Invalid Docker host URL configuration:", error); - throw new Error("Invalid Docker host configuration"); - } + if (Number.isNaN(port) || port < 1 || port > 65535) { + throw new Error("Invalid port number in Docker host URL"); + } + + return new Docker({ + protocol: host.secure ? "https" : "http", + host: hostAddress, + port, + version: "v1.41", + // TODO: Add TLS configuration if needed + }); + } catch (error) { + logger.error("Invalid Docker host URL configuration:", error); + throw new Error("Invalid Docker host configuration"); + } }; diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index 112187b4..3453811d 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -2,122 +2,123 @@ import { dbFunctions } from "~/core/database"; import storeContainerData from "~/core/docker/store-container-stats"; import storeHostData from "~/core/docker/store-host-stats"; import { logger } from "~/core/utils/logger"; -import type { config } from "~/typings/database"; +import type { config } from "../../../typings/database"; function convertFromMinToMs(minutes: number): number { - return minutes * 60 * 1000; + return minutes * 60 * 1000; } async function initialRun( - scheduleName: string, - scheduleFunction: Promise | void, - isAsync: boolean, + scheduleName: string, + scheduleFunction: Promise | void, + isAsync: boolean ) { - try { - if (isAsync) { - await scheduleFunction; - } else { - scheduleFunction; - } - logger.info(`Startup run success for: ${scheduleName}`); - } catch (error) { - logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); - } + try { + if (isAsync) { + await scheduleFunction; + } else { + scheduleFunction; + } + logger.info(`Startup run success for: ${scheduleName}`); + } catch (error) { + logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); + } } async function scheduledJob( - name: string, - jobFn: () => Promise, - intervalMs: number, + name: string, + jobFn: () => Promise, + intervalMs: number ) { - while (true) { - const start = Date.now(); - logger.info(`Task Start: ${name}`); - try { - await jobFn(); - logger.info(`Task End: ${name} succeeded.`); - } catch (e) { - logger.error(`Task End: ${name} failed:`, e); - } - const elapsed = Date.now() - start; - const delay = Math.max(0, intervalMs - elapsed); - await new Promise((r) => setTimeout(r, delay)); - } + while (true) { + const start = Date.now(); + logger.info(`Task Start: ${name}`); + try { + await jobFn(); + logger.info(`Task End: ${name} succeeded.`); + } catch (e) { + logger.error(`Task End: ${name} failed:`, e); + } + const elapsed = Date.now() - start; + const delay = Math.max(0, intervalMs - elapsed); + await new Promise((r) => setTimeout(r, delay)); + } } async function setSchedules() { - try { - const rawConfigData: unknown[] = dbFunctions.getConfig(); - const configData = rawConfigData[0]; - - if ( - !configData || - typeof (configData as config).keep_data_for !== "number" || - typeof (configData as config).fetching_interval !== "number" - ) { - logger.error("Invalid configuration data:", configData); - throw new Error("Invalid configuration data"); - } - - const { keep_data_for, fetching_interval } = configData as config; - - if (keep_data_for === undefined) { - const errMsg = "keep_data_for is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } - - if (fetching_interval === undefined) { - const errMsg = "fetching_interval is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } - - logger.info( - `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, - ); - - logger.info( - `Scheduling: Updating host statistics every ${fetching_interval} minutes`, - ); - - logger.info( - `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, - ); - - // Schedule container data fetching - await initialRun("storeContainerData", storeContainerData(), true); - scheduledJob( - "storeContainerData", - storeContainerData, - convertFromMinToMs(fetching_interval), - ); - - // Schedule Host statistics updates - await initialRun("storeHostData", storeHostData(), true); - scheduledJob( - "storeHostData", - storeHostData, - convertFromMinToMs(fetching_interval), - ); - - // Schedule database cleanup - await initialRun( - "dbFunctions.deleteOldData", - dbFunctions.deleteOldData(keep_data_for), - false, - ); - scheduledJob( - "cleanupOldData", - () => Promise.resolve(dbFunctions.deleteOldData(keep_data_for)), - convertFromMinToMs(60), - ); - - logger.info("Schedules have been set successfully."); - } catch (error) { - logger.error("Error setting schedules:", error); - throw error; - } + logger.info("Starting DockStatAPI"); + try { + const rawConfigData: unknown[] = dbFunctions.getConfig(); + const configData = rawConfigData[0]; + + if ( + !configData || + typeof (configData as config).keep_data_for !== "number" || + typeof (configData as config).fetching_interval !== "number" + ) { + logger.error("Invalid configuration data:", configData); + throw new Error("Invalid configuration data"); + } + + const { keep_data_for, fetching_interval } = configData as config; + + if (keep_data_for === undefined) { + const errMsg = "keep_data_for is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + if (fetching_interval === undefined) { + const errMsg = "fetching_interval is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + logger.info( + `Scheduling: Fetching container statistics every ${fetching_interval} minutes` + ); + + logger.info( + `Scheduling: Updating host statistics every ${fetching_interval} minutes` + ); + + logger.info( + `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days` + ); + + // Schedule container data fetching + await initialRun("storeContainerData", storeContainerData(), true); + scheduledJob( + "storeContainerData", + storeContainerData, + convertFromMinToMs(fetching_interval) + ); + + // Schedule Host statistics updates + await initialRun("storeHostData", storeHostData(), true); + scheduledJob( + "storeHostData", + storeHostData, + convertFromMinToMs(fetching_interval) + ); + + // Schedule database cleanup + await initialRun( + "dbFunctions.deleteOldData", + dbFunctions.deleteOldData(keep_data_for), + false + ); + scheduledJob( + "cleanupOldData", + () => Promise.resolve(dbFunctions.deleteOldData(keep_data_for)), + convertFromMinToMs(60) + ); + + logger.info("Schedules have been set successfully."); + } catch (error) { + logger.error("Error setting schedules:", error); + throw error; + } } export { setSchedules }; diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index c22c3d59..383acfdb 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -1,172 +1,172 @@ import { EventEmitter } from "node:events"; -import type { ContainerInfo } from "~/typings/docker"; -import type { Hooks, Plugin, PluginInfo } from "~/typings/plugin"; +import type { ContainerInfo } from "../../../typings/docker"; +import type { Plugin, PluginInfo } from "../../../typings/plugin"; import { logger } from "../utils/logger"; function getHooks(plugin: Plugin) { - return { - onContainerStart: !!plugin.onContainerStart, - onContainerStop: !!plugin.onContainerStop, - onContainerExit: !!plugin.onContainerExit, - onContainerCreate: !!plugin.onContainerCreate, - onContainerKill: !!plugin.onContainerKill, - handleContainerDie: !!plugin.handleContainerDie, - onContainerDestroy: !!plugin.onContainerDestroy, - onContainerPause: !!plugin.onContainerPause, - onContainerUnpause: !!plugin.onContainerUnpause, - onContainerRestart: !!plugin.onContainerRestart, - onContainerUpdate: !!plugin.onContainerUpdate, - onContainerRename: !!plugin.onContainerRename, - onContainerHealthStatus: !!plugin.onContainerHealthStatus, - onHostUnreachable: !!plugin.onHostUnreachable, - onHostReachableAgain: !!plugin.onHostReachableAgain, - }; + return { + onContainerStart: !!plugin.onContainerStart, + onContainerStop: !!plugin.onContainerStop, + onContainerExit: !!plugin.onContainerExit, + onContainerCreate: !!plugin.onContainerCreate, + onContainerKill: !!plugin.onContainerKill, + handleContainerDie: !!plugin.handleContainerDie, + onContainerDestroy: !!plugin.onContainerDestroy, + onContainerPause: !!plugin.onContainerPause, + onContainerUnpause: !!plugin.onContainerUnpause, + onContainerRestart: !!plugin.onContainerRestart, + onContainerUpdate: !!plugin.onContainerUpdate, + onContainerRename: !!plugin.onContainerRename, + onContainerHealthStatus: !!plugin.onContainerHealthStatus, + onHostUnreachable: !!plugin.onHostUnreachable, + onHostReachableAgain: !!plugin.onHostReachableAgain, + }; } class PluginManager extends EventEmitter { - private plugins: Map = new Map(); - private failedPlugins: Map = new Map(); - - fail(plugin: Plugin) { - try { - this.failedPlugins.set(plugin.name, plugin); - logger.debug(`Set status to failed for plugin: ${plugin.name}`); - } catch (error) { - logger.error(`Adding failed plugin to list failed: ${error as string}`); - } - } - - register(plugin: Plugin) { - try { - this.plugins.set(plugin.name, plugin); - logger.debug(`Registered plugin: ${plugin.name}`); - } catch (error) { - logger.error( - `Registering plugin ${plugin.name} failed: ${error as string}`, - ); - } - } - - unregister(name: string) { - this.plugins.delete(name); - } - - getPlugins(): PluginInfo[] { - const loadedPlugins = Array.from(this.plugins.values()).map((plugin) => { - const hooks: Hooks = getHooks(plugin); - - return { - name: plugin.name, - status: "active", - usedHooks: hooks, - }; - }); - - const failedPlugins = Array.from(this.failedPlugins.values()).map( - (plugin) => { - const hooks: Hooks = getHooks(plugin); - - return { - name: plugin.name, - status: "inactive", - usedHooks: hooks, - }; - }, - ); - - return loadedPlugins.concat(failedPlugins); - } - - // Trigger plugin flows: - handleContainerStop(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerStop?.(containerInfo); - } - } - - handleContainerStart(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerStart?.(containerInfo); - } - } - - handleContainerExit(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerExit?.(containerInfo); - } - } - - handleContainerCreate(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerCreate?.(containerInfo); - } - } - - handleContainerDestroy(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerDestroy?.(containerInfo); - } - } - - handleContainerPause(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerPause?.(containerInfo); - } - } - - handleContainerUnpause(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerUnpause?.(containerInfo); - } - } - - handleContainerRestart(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerRestart?.(containerInfo); - } - } - - handleContainerUpdate(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerUpdate?.(containerInfo); - } - } - - handleContainerRename(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerRename?.(containerInfo); - } - } - - handleContainerHealthStatus(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerHealthStatus?.(containerInfo); - } - } - - handleHostUnreachable(host: string, err: string) { - for (const [, plugin] of this.plugins) { - plugin.onHostUnreachable?.(host, err); - } - } - - handleHostReachableAgain(host: string) { - for (const [, plugin] of this.plugins) { - plugin.onHostReachableAgain?.(host); - } - } - - handleContainerKill(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerKill?.(containerInfo); - } - } - - handleContainerDie(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.handleContainerDie?.(containerInfo); - } - } + private plugins: Map = new Map(); + private failedPlugins: Map = new Map(); + + fail(plugin: Plugin) { + try { + this.failedPlugins.set(plugin.name, plugin); + logger.debug(`Set status to failed for plugin: ${plugin.name}`); + } catch (error) { + logger.error(`Adding failed plugin to list failed: ${error as string}`); + } + } + + register(plugin: Plugin) { + try { + this.plugins.set(plugin.name, plugin); + logger.debug(`Registered plugin: ${plugin.name}`); + } catch (error) { + logger.error( + `Registering plugin ${plugin.name} failed: ${error as string}` + ); + } + } + + unregister(name: string) { + this.plugins.delete(name); + } + + getPlugins(): PluginInfo[] { + const loadedPlugins = Array.from(this.plugins.values()).map((plugin) => { + logger.debug(`Loaded plugin: ${plugin}`); + const hooks = getHooks(plugin); + return { + name: plugin.name, + status: "active", + usedHooks: hooks, + }; + }); + + const failedPlugins = Array.from(this.failedPlugins.values()).map( + (plugin) => { + const hooks = getHooks(plugin); + + return { + name: plugin.name, + status: "inactive", + usedHooks: hooks, + }; + } + ); + + return loadedPlugins.concat(failedPlugins); + } + + // Trigger plugin flows: + handleContainerStop(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStop?.(containerInfo); + } + } + + handleContainerStart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStart?.(containerInfo); + } + } + + handleContainerExit(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerExit?.(containerInfo); + } + } + + handleContainerCreate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerCreate?.(containerInfo); + } + } + + handleContainerDestroy(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerDestroy?.(containerInfo); + } + } + + handleContainerPause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerPause?.(containerInfo); + } + } + + handleContainerUnpause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUnpause?.(containerInfo); + } + } + + handleContainerRestart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRestart?.(containerInfo); + } + } + + handleContainerUpdate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUpdate?.(containerInfo); + } + } + + handleContainerRename(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRename?.(containerInfo); + } + } + + handleContainerHealthStatus(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerHealthStatus?.(containerInfo); + } + } + + handleHostUnreachable(host: string, err: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostUnreachable?.(host, err); + } + } + + handleHostReachableAgain(host: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostReachableAgain?.(host); + } + } + + handleContainerKill(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerKill?.(containerInfo); + } + } + + handleContainerDie(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.handleContainerDie?.(containerInfo); + } + } } export const pluginManager = new PluginManager(); diff --git a/src/handlers/config.ts b/src/handlers/config.ts index e02a49dd..d87c84a1 100644 --- a/src/handlers/config.ts +++ b/src/handlers/config.ts @@ -4,181 +4,182 @@ import { backupDir } from "~/core/database/backup"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; -import type { config } from "~/typings/database"; -import type { DockerHost } from "~/typings/docker"; +import type { config } from "../../typings/database"; +import type { DockerHost } from "../../typings/docker"; +import type { PluginInfo } from "../../typings/plugin"; class apiHandler { - async getConfig() { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateConfig(fetching_interval: number, keep_data_for: number) { - try { - dbFunctions.updateConfig(fetching_interval, keep_data_for); - return "Updated DockStatAPI config"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getPlugins() { - try { - return pluginManager.getPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getPackage() { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json`, - ); - - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } - - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async createbackup() { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return backupFilename; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async listBackups() { - try { - const backupFiles = readdirSync(backupDir); - - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.startsWith(".") || - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); - - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async downloadbackup(downloadFile?: string) { - try { - const filename: string = downloadFile || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; - - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } - - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async restoreBackup(file: Bun.FileBlob) { - try { - if (!file) { - throw new Error("No file uploaded"); - } - - if (!(file.name || "").endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } - - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); - - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); - - return "Database restored successfully"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async addHost(host: DockerHost) { - try { - dbFunctions.addDockerHost(host); - return `Added docker host (${host.name} - ${host.hostAddress})`; - } catch (error: unknown) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateHost(host: DockerHost) { - try { - dbFunctions.updateDockerHost(host); - return `Updated docker host (${host.id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async removeHost(id: number) { - try { - dbFunctions.deleteDockerHost(id); - return `Deleted docker host (${id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } + getConfig() { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateConfig(fetching_interval: number, keep_data_for: number) { + try { + dbFunctions.updateConfig(fetching_interval, keep_data_for); + return "Updated DockStatAPI config"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + getPlugins(): PluginInfo[] { + try { + return pluginManager.getPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getPackage() { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json` + ); + + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } + + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async createbackup() { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return backupFilename; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async listBackups() { + try { + const backupFiles = readdirSync(backupDir); + + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.startsWith(".") || + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); + + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async downloadbackup(downloadFile?: string) { + try { + const filename: string = downloadFile || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; + + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } + + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async restoreBackup(file: Bun.FileBlob) { + try { + if (!file) { + throw new Error("No file uploaded"); + } + + if (!(file.name || "").endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } + + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return "Database restored successfully"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async addHost(host: DockerHost) { + try { + dbFunctions.addDockerHost(host); + return `Added docker host (${host.name} - ${host.hostAddress})`; + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateHost(host: DockerHost) { + try { + dbFunctions.updateDockerHost(host); + return `Updated docker host (${host.id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async removeHost(id: number) { + try { + dbFunctions.deleteDockerHost(id); + return `Deleted docker host (${id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } } export const ApiHandler = new apiHandler(); diff --git a/src/handlers/docker.ts b/src/handlers/docker.ts index 7606f56d..924c6f35 100644 --- a/src/handlers/docker.ts +++ b/src/handlers/docker.ts @@ -3,154 +3,158 @@ import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; -import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; -import type { DockerInfo } from "~/typings/dockerode"; +import type { + ContainerInfo, + DockerHost, + HostStats, +} from "../../typings/docker"; +import type { DockerInfo } from "../../typings/dockerode"; class basicDockerHandler { - async getContainers() { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; - - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - throw new Error(pingError as string); - } - - const hostContainers = await docker.listContainers({ all: true }); - - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - throw new Error(error as string); - } - if (!stats) { - throw new Error("No stats available"); - } - resolve(stats); - }); - }, - ); - - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: stats.cpu_stats.system_cpu_usage, - memoryUsage: stats.memory_stats.usage, - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError, - ); - } - }), - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }), - ); - - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getHostStats(id?: number) { - if (!id) { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - - const stats: HostStats[] = []; - - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - stats.push(config); - } - - logger.debug("Fetched all hosts"); - return stats; - } catch (error) { - throw new Error(error as string); - } - } - - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - - const host = findObjectByKey(hosts, "id", Number(id)); - if (!host) { - throw new Error(`Host (${id}) not found`); - } - - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - throw new Error("Failed to retrieve host config"); - } - } + async getContainers() { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; + + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + throw new Error(pingError as string); + } + + const hostContainers = await docker.listContainers({ all: true }); + + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + throw new Error(error as string); + } + if (!stats) { + throw new Error("No stats available"); + } + resolve(stats); + }); + } + ); + + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: stats.cpu_stats.system_cpu_usage, + memoryUsage: stats.memory_stats.usage, + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError + ); + } + }) + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }) + ); + + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getHostStats(id?: number) { + if (!id) { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + const stats: HostStats[] = []; + + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + stats.push(config); + } + + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + throw new Error(error as string); + } + } + + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + const host = findObjectByKey(hosts, "id", Number(id)); + if (!host) { + throw new Error(`Host (${id}) not found`); + } + + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + throw new Error("Failed to retrieve host config"); + } + } } export const BasicDockerHandler = new basicDockerHandler(); diff --git a/src/handlers/index.ts b/src/handlers/index.ts index b65a5d20..38fd387c 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -8,12 +8,12 @@ import { StackHandler } from "./stacks"; import { CheckHealth } from "./utils"; export const handlers = { - BasicDockerHandler, - ApiHandler, - DatabaseHandler, - StackHandler, - LogHandler, - CheckHealth, - sockets: Sockets, - start: setSchedules, + BasicDockerHandler, + ApiHandler, + DatabaseHandler, + StackHandler, + LogHandler, + CheckHealth, + Sockets: Sockets, + Start: setSchedules(), }; diff --git a/src/handlers/modules/docker-socket.ts b/src/handlers/modules/docker-socket.ts index 080ee2bd..a6301564 100644 --- a/src/handlers/modules/docker-socket.ts +++ b/src/handlers/modules/docker-socket.ts @@ -3,141 +3,140 @@ import split2 from "split2"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; -import type { DockerStatsEvent } from "~/typings/docker"; -import { ContainerInfo } from "~/typings/docker"; +import type { DockerStatsEvent } from "../../../typings/docker"; export function createDockerStatsStream(): Readable { - const stream = new Readable({ - objectMode: true, - read() {}, - }); + const stream = new Readable({ + objectMode: true, + read() {}, + }); - const substreams: Array<{ - statsStream: Readable; - splitStream: Transform; - }> = []; + const substreams: Array<{ + statsStream: Readable; + splitStream: Transform; + }> = []; - const cleanup = () => { - for (const { statsStream, splitStream } of substreams) { - try { - statsStream.unpipe(splitStream); - statsStream.destroy(); - splitStream.destroy(); - } catch (error) { - logger.error(`Cleanup error: ${error}`); - } - } - substreams.length = 0; - }; + const cleanup = () => { + for (const { statsStream, splitStream } of substreams) { + try { + statsStream.unpipe(splitStream); + statsStream.destroy(); + splitStream.destroy(); + } catch (error) { + logger.error(`Cleanup error: ${error}`); + } + } + substreams.length = 0; + }; - stream.on("close", cleanup); - stream.on("error", cleanup); + stream.on("close", cleanup); + stream.on("error", cleanup); - (async () => { - try { - const hosts = dbFunctions.getDockerHosts(); - logger.debug(`Retrieved ${hosts.length} docker host(s)`); + (async () => { + try { + const hosts = dbFunctions.getDockerHosts(); + logger.debug(`Retrieved ${hosts.length} docker host(s)`); - for (const host of hosts) { - if (stream.destroyed) break; + for (const host of hosts) { + if (stream.destroyed) break; - try { - const docker = getDockerClient(host); - await docker.ping(); - const containers = await docker.listContainers({ - all: true, - }); + try { + const docker = getDockerClient(host); + await docker.ping(); + const containers = await docker.listContainers({ + all: true, + }); - logger.debug( - `Found ${containers.length} containers on ${host.name} (id: ${host.id})`, - ); + logger.debug( + `Found ${containers.length} containers on ${host.name} (id: ${host.id})` + ); - for (const containerInfo of containers) { - if (stream.destroyed) break; + for (const containerInfo of containers) { + if (stream.destroyed) break; - try { - const container = docker.getContainer(containerInfo.Id); - const statsStream = (await container.stats({ - stream: true, - })) as Readable; - const splitStream = split2(); + try { + const container = docker.getContainer(containerInfo.Id); + const statsStream = (await container.stats({ + stream: true, + })) as Readable; + const splitStream = split2(); - substreams.push({ statsStream, splitStream }); + substreams.push({ statsStream, splitStream }); - statsStream - .on("close", () => splitStream.destroy()) - .pipe(splitStream) - .on("data", (line: string) => { - if (stream.destroyed || !line) return; + statsStream + .on("close", () => splitStream.destroy()) + .pipe(splitStream) + .on("data", (line: string) => { + if (stream.destroyed || !line) return; - try { - const stats = JSON.parse(line); - const event: DockerStatsEvent = { - type: "stats", - id: containerInfo.Id, - hostId: host.id, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats) ?? 0, - memoryUsage: calculateMemoryUsage(stats) ?? 0, - }; - stream.push(event); - } catch (error) { - stream.push({ - type: "error", - hostId: host.id, - containerId: containerInfo.Id, - error: `Parse error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); - } - }) - .on("error", (error: Error) => { - stream.push({ - type: "error", - hostId: host.id, - containerId: containerInfo.Id, - error: `Stream error: ${error.message}`, - }); - }); - } catch (error) { - stream.push({ - type: "error", - hostId: host.id, - containerId: containerInfo.Id, - error: `Container error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); - } - } - } catch (error) { - stream.push({ - type: "error", - hostId: host.id, - error: `Host connection error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); - } - } - } catch (error) { - stream.push({ - type: "error", - error: `Initialization error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); - stream.destroy(); - } - })(); + try { + const stats = JSON.parse(line); + const event: DockerStatsEvent = { + type: "stats", + id: containerInfo.Id, + hostId: host.id, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats) ?? 0, + memoryUsage: calculateMemoryUsage(stats) ?? 0, + }; + stream.push(event); + } catch (error) { + stream.push({ + type: "error", + hostId: host.id, + containerId: containerInfo.Id, + error: `Parse error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + }) + .on("error", (error: Error) => { + stream.push({ + type: "error", + hostId: host.id, + containerId: containerInfo.Id, + error: `Stream error: ${error.message}`, + }); + }); + } catch (error) { + stream.push({ + type: "error", + hostId: host.id, + containerId: containerInfo.Id, + error: `Container error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + } + } catch (error) { + stream.push({ + type: "error", + hostId: host.id, + error: `Host connection error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + } + } catch (error) { + stream.push({ + type: "error", + error: `Initialization error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + stream.destroy(); + } + })(); - return stream; + return stream; } diff --git a/src/handlers/stacks.ts b/src/handlers/stacks.ts index abdbb8e0..f064bdad 100644 --- a/src/handlers/stacks.ts +++ b/src/handlers/stacks.ts @@ -1,127 +1,127 @@ import { dbFunctions } from "~/core/database"; import { - deployStack, - getAllStacksStatus, - getStackStatus, - pullStackImages, - removeStack, - restartStack, - startStack, - stopStack, + deployStack, + getAllStacksStatus, + getStackStatus, + pullStackImages, + removeStack, + restartStack, + startStack, + stopStack, } from "~/core/stacks/controller"; import { logger } from "~/core/utils/logger"; import type { stacks_config } from "~/typings/database"; class stackHandler { - async deploy(config: stacks_config) { - try { - await deployStack(config); - logger.info(`Deployed Stack (${config.name})`); - return `Stack ${config.name} deployed successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error deploying stack, please check the server logs for more information`; - } - } - - async start(stackId: number) { - try { - if (!stackId) { - throw new Error("Stack ID needed"); - } - await startStack(stackId); - logger.info(`Started Stack (${stackId})`); - return `Stack ${stackId} started successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error starting stack`; - } - } - - async stop(stackId: number) { - try { - if (!stackId) { - throw new Error("Stack needed"); - } - await stopStack(stackId); - logger.info(`Stopped Stack (${stackId})`); - return `Stack ${stackId} stopped successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error stopping stack`; - } - } - - async restart(stackId: number) { - try { - if (!stackId) { - throw new Error("StackID needed"); - } - await restartStack(stackId); - logger.info(`Restarted Stack (${stackId})`); - return `Stack ${stackId} restarted successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error restarting stack`; - } - } - - async pullImages(stackId: number) { - try { - if (!stackId) { - throw new Error("StackID needed"); - } - await pullStackImages(stackId); - logger.info(`Pulled Stack images (${stackId})`); - return `Images for stack ${stackId} pulled successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error pulling images`; - } - } - - async getStatus(stackId?: number) { - if (stackId) { - const status = await getStackStatus(stackId); - logger.debug( - `Retrieved status for stackId=${stackId}: ${JSON.stringify(status)}`, - ); - return status; - } - - logger.debug("Fetching status for all stacks"); - const status = await getAllStacksStatus(); - logger.debug(`Retrieved status for all stacks: ${JSON.stringify(status)}`); - - return status; - } - - async listStacks() { - try { - const stacks = dbFunctions.getStacks(); - logger.info("Fetched Stacks"); - return stacks; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - return `${errorMsg}, Error getting stacks`; - } - } - - async deleteStack(stackId: number) { - try { - await removeStack(stackId); - logger.info(`Deleted Stack ${stackId}`); - return `Stack ${stackId} deleted successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - return `${errorMsg}, Error deleting stack`; - } - } + async deploy(config: stacks_config) { + try { + await deployStack(config); + logger.info(`Deployed Stack (${config.name})`); + return `Stack ${config.name} deployed successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error deploying stack, please check the server logs for more information`; + } + } + + async start(stackId: number) { + try { + if (!stackId) { + throw new Error("Stack ID needed"); + } + await startStack(stackId); + logger.info(`Started Stack (${stackId})`); + return `Stack ${stackId} started successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error starting stack`; + } + } + + async stop(stackId: number) { + try { + if (!stackId) { + throw new Error("Stack needed"); + } + await stopStack(stackId); + logger.info(`Stopped Stack (${stackId})`); + return `Stack ${stackId} stopped successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error stopping stack`; + } + } + + async restart(stackId: number) { + try { + if (!stackId) { + throw new Error("StackID needed"); + } + await restartStack(stackId); + logger.info(`Restarted Stack (${stackId})`); + return `Stack ${stackId} restarted successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error restarting stack`; + } + } + + async pullImages(stackId: number) { + try { + if (!stackId) { + throw new Error("StackID needed"); + } + await pullStackImages(stackId); + logger.info(`Pulled Stack images (${stackId})`); + return `Images for stack ${stackId} pulled successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error pulling images`; + } + } + + async getStatus(stackId?: number) { + if (stackId) { + const status = await getStackStatus(stackId); + logger.debug( + `Retrieved status for stackId=${stackId}: ${JSON.stringify(status)}` + ); + return status; + } + + logger.debug("Fetching status for all stacks"); + const status = await getAllStacksStatus(); + logger.debug(`Retrieved status for all stacks: ${JSON.stringify(status)}`); + + return status; + } + + listStacks(): stacks_config[] { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks"); + return stacks; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + throw new Error(`${errorMsg}, Error getting stacks`); + } + } + + async deleteStack(stackId: number) { + try { + await removeStack(stackId); + logger.info(`Deleted Stack ${stackId}`); + return `Stack ${stackId} deleted successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return `${errorMsg}, Error deleting stack`; + } + } } export const StackHandler = new stackHandler(); diff --git a/src/handlers/utils.ts b/src/handlers/utils.ts index 8d75f7be..206f5424 100644 --- a/src/handlers/utils.ts +++ b/src/handlers/utils.ts @@ -1,3 +1,6 @@ +import { logger } from "~/core/utils/logger"; + export async function CheckHealth() { - return "healthy"; + logger.info("Checking health"); + return "healthy"; } From c6931bcc4569b538e74a8542d2a9a18363be9b7b Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 30 Jun 2025 11:56:26 +0000 Subject: [PATCH 343/369] CQL: Apply lint fixes [skip ci] --- src/core/docker/client.ts | 52 ++-- src/core/docker/scheduler.ts | 206 ++++++++-------- src/core/plugins/plugin-manager.ts | 320 ++++++++++++------------ src/handlers/config.ts | 340 +++++++++++++------------- src/handlers/docker.ts | 294 +++++++++++----------- src/handlers/index.ts | 16 +- src/handlers/modules/docker-socket.ts | 236 +++++++++--------- src/handlers/stacks.ts | 234 +++++++++--------- src/handlers/utils.ts | 4 +- 9 files changed, 851 insertions(+), 851 deletions(-) diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index cd252d68..35876a6e 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -3,33 +3,33 @@ import { logger } from "~/core/utils/logger"; import type { DockerHost } from "../../../typings/docker"; export const getDockerClient = (host: DockerHost): Docker => { - try { - logger.info(`Setting up host: ${JSON.stringify(host)}`); + try { + logger.info(`Setting up host: ${JSON.stringify(host)}`); - const inputUrl = host.hostAddress.includes("://") - ? host.hostAddress - : `${host.secure ? "https" : "http"}://${host.hostAddress}`; - const parsedUrl = new URL(inputUrl); - const hostAddress = parsedUrl.hostname; - const port = parsedUrl.port - ? Number.parseInt(parsedUrl.port) - : host.secure - ? 2376 - : 2375; + const inputUrl = host.hostAddress.includes("://") + ? host.hostAddress + : `${host.secure ? "https" : "http"}://${host.hostAddress}`; + const parsedUrl = new URL(inputUrl); + const hostAddress = parsedUrl.hostname; + const port = parsedUrl.port + ? Number.parseInt(parsedUrl.port) + : host.secure + ? 2376 + : 2375; - if (Number.isNaN(port) || port < 1 || port > 65535) { - throw new Error("Invalid port number in Docker host URL"); - } + if (Number.isNaN(port) || port < 1 || port > 65535) { + throw new Error("Invalid port number in Docker host URL"); + } - return new Docker({ - protocol: host.secure ? "https" : "http", - host: hostAddress, - port, - version: "v1.41", - // TODO: Add TLS configuration if needed - }); - } catch (error) { - logger.error("Invalid Docker host URL configuration:", error); - throw new Error("Invalid Docker host configuration"); - } + return new Docker({ + protocol: host.secure ? "https" : "http", + host: hostAddress, + port, + version: "v1.41", + // TODO: Add TLS configuration if needed + }); + } catch (error) { + logger.error("Invalid Docker host URL configuration:", error); + throw new Error("Invalid Docker host configuration"); + } }; diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index 3453811d..8754fd66 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -5,120 +5,120 @@ import { logger } from "~/core/utils/logger"; import type { config } from "../../../typings/database"; function convertFromMinToMs(minutes: number): number { - return minutes * 60 * 1000; + return minutes * 60 * 1000; } async function initialRun( - scheduleName: string, - scheduleFunction: Promise | void, - isAsync: boolean + scheduleName: string, + scheduleFunction: Promise | void, + isAsync: boolean, ) { - try { - if (isAsync) { - await scheduleFunction; - } else { - scheduleFunction; - } - logger.info(`Startup run success for: ${scheduleName}`); - } catch (error) { - logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); - } + try { + if (isAsync) { + await scheduleFunction; + } else { + scheduleFunction; + } + logger.info(`Startup run success for: ${scheduleName}`); + } catch (error) { + logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); + } } async function scheduledJob( - name: string, - jobFn: () => Promise, - intervalMs: number + name: string, + jobFn: () => Promise, + intervalMs: number, ) { - while (true) { - const start = Date.now(); - logger.info(`Task Start: ${name}`); - try { - await jobFn(); - logger.info(`Task End: ${name} succeeded.`); - } catch (e) { - logger.error(`Task End: ${name} failed:`, e); - } - const elapsed = Date.now() - start; - const delay = Math.max(0, intervalMs - elapsed); - await new Promise((r) => setTimeout(r, delay)); - } + while (true) { + const start = Date.now(); + logger.info(`Task Start: ${name}`); + try { + await jobFn(); + logger.info(`Task End: ${name} succeeded.`); + } catch (e) { + logger.error(`Task End: ${name} failed:`, e); + } + const elapsed = Date.now() - start; + const delay = Math.max(0, intervalMs - elapsed); + await new Promise((r) => setTimeout(r, delay)); + } } async function setSchedules() { - logger.info("Starting DockStatAPI"); - try { - const rawConfigData: unknown[] = dbFunctions.getConfig(); - const configData = rawConfigData[0]; - - if ( - !configData || - typeof (configData as config).keep_data_for !== "number" || - typeof (configData as config).fetching_interval !== "number" - ) { - logger.error("Invalid configuration data:", configData); - throw new Error("Invalid configuration data"); - } - - const { keep_data_for, fetching_interval } = configData as config; - - if (keep_data_for === undefined) { - const errMsg = "keep_data_for is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } - - if (fetching_interval === undefined) { - const errMsg = "fetching_interval is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } - - logger.info( - `Scheduling: Fetching container statistics every ${fetching_interval} minutes` - ); - - logger.info( - `Scheduling: Updating host statistics every ${fetching_interval} minutes` - ); - - logger.info( - `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days` - ); - - // Schedule container data fetching - await initialRun("storeContainerData", storeContainerData(), true); - scheduledJob( - "storeContainerData", - storeContainerData, - convertFromMinToMs(fetching_interval) - ); - - // Schedule Host statistics updates - await initialRun("storeHostData", storeHostData(), true); - scheduledJob( - "storeHostData", - storeHostData, - convertFromMinToMs(fetching_interval) - ); - - // Schedule database cleanup - await initialRun( - "dbFunctions.deleteOldData", - dbFunctions.deleteOldData(keep_data_for), - false - ); - scheduledJob( - "cleanupOldData", - () => Promise.resolve(dbFunctions.deleteOldData(keep_data_for)), - convertFromMinToMs(60) - ); - - logger.info("Schedules have been set successfully."); - } catch (error) { - logger.error("Error setting schedules:", error); - throw error; - } + logger.info("Starting DockStatAPI"); + try { + const rawConfigData: unknown[] = dbFunctions.getConfig(); + const configData = rawConfigData[0]; + + if ( + !configData || + typeof (configData as config).keep_data_for !== "number" || + typeof (configData as config).fetching_interval !== "number" + ) { + logger.error("Invalid configuration data:", configData); + throw new Error("Invalid configuration data"); + } + + const { keep_data_for, fetching_interval } = configData as config; + + if (keep_data_for === undefined) { + const errMsg = "keep_data_for is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + if (fetching_interval === undefined) { + const errMsg = "fetching_interval is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + logger.info( + `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, + ); + + logger.info( + `Scheduling: Updating host statistics every ${fetching_interval} minutes`, + ); + + logger.info( + `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, + ); + + // Schedule container data fetching + await initialRun("storeContainerData", storeContainerData(), true); + scheduledJob( + "storeContainerData", + storeContainerData, + convertFromMinToMs(fetching_interval), + ); + + // Schedule Host statistics updates + await initialRun("storeHostData", storeHostData(), true); + scheduledJob( + "storeHostData", + storeHostData, + convertFromMinToMs(fetching_interval), + ); + + // Schedule database cleanup + await initialRun( + "dbFunctions.deleteOldData", + dbFunctions.deleteOldData(keep_data_for), + false, + ); + scheduledJob( + "cleanupOldData", + () => Promise.resolve(dbFunctions.deleteOldData(keep_data_for)), + convertFromMinToMs(60), + ); + + logger.info("Schedules have been set successfully."); + } catch (error) { + logger.error("Error setting schedules:", error); + throw error; + } } export { setSchedules }; diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index 383acfdb..e0bf121e 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -4,169 +4,169 @@ import type { Plugin, PluginInfo } from "../../../typings/plugin"; import { logger } from "../utils/logger"; function getHooks(plugin: Plugin) { - return { - onContainerStart: !!plugin.onContainerStart, - onContainerStop: !!plugin.onContainerStop, - onContainerExit: !!plugin.onContainerExit, - onContainerCreate: !!plugin.onContainerCreate, - onContainerKill: !!plugin.onContainerKill, - handleContainerDie: !!plugin.handleContainerDie, - onContainerDestroy: !!plugin.onContainerDestroy, - onContainerPause: !!plugin.onContainerPause, - onContainerUnpause: !!plugin.onContainerUnpause, - onContainerRestart: !!plugin.onContainerRestart, - onContainerUpdate: !!plugin.onContainerUpdate, - onContainerRename: !!plugin.onContainerRename, - onContainerHealthStatus: !!plugin.onContainerHealthStatus, - onHostUnreachable: !!plugin.onHostUnreachable, - onHostReachableAgain: !!plugin.onHostReachableAgain, - }; + return { + onContainerStart: !!plugin.onContainerStart, + onContainerStop: !!plugin.onContainerStop, + onContainerExit: !!plugin.onContainerExit, + onContainerCreate: !!plugin.onContainerCreate, + onContainerKill: !!plugin.onContainerKill, + handleContainerDie: !!plugin.handleContainerDie, + onContainerDestroy: !!plugin.onContainerDestroy, + onContainerPause: !!plugin.onContainerPause, + onContainerUnpause: !!plugin.onContainerUnpause, + onContainerRestart: !!plugin.onContainerRestart, + onContainerUpdate: !!plugin.onContainerUpdate, + onContainerRename: !!plugin.onContainerRename, + onContainerHealthStatus: !!plugin.onContainerHealthStatus, + onHostUnreachable: !!plugin.onHostUnreachable, + onHostReachableAgain: !!plugin.onHostReachableAgain, + }; } class PluginManager extends EventEmitter { - private plugins: Map = new Map(); - private failedPlugins: Map = new Map(); - - fail(plugin: Plugin) { - try { - this.failedPlugins.set(plugin.name, plugin); - logger.debug(`Set status to failed for plugin: ${plugin.name}`); - } catch (error) { - logger.error(`Adding failed plugin to list failed: ${error as string}`); - } - } - - register(plugin: Plugin) { - try { - this.plugins.set(plugin.name, plugin); - logger.debug(`Registered plugin: ${plugin.name}`); - } catch (error) { - logger.error( - `Registering plugin ${plugin.name} failed: ${error as string}` - ); - } - } - - unregister(name: string) { - this.plugins.delete(name); - } - - getPlugins(): PluginInfo[] { - const loadedPlugins = Array.from(this.plugins.values()).map((plugin) => { - logger.debug(`Loaded plugin: ${plugin}`); - const hooks = getHooks(plugin); - return { - name: plugin.name, - status: "active", - usedHooks: hooks, - }; - }); - - const failedPlugins = Array.from(this.failedPlugins.values()).map( - (plugin) => { - const hooks = getHooks(plugin); - - return { - name: plugin.name, - status: "inactive", - usedHooks: hooks, - }; - } - ); - - return loadedPlugins.concat(failedPlugins); - } - - // Trigger plugin flows: - handleContainerStop(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerStop?.(containerInfo); - } - } - - handleContainerStart(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerStart?.(containerInfo); - } - } - - handleContainerExit(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerExit?.(containerInfo); - } - } - - handleContainerCreate(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerCreate?.(containerInfo); - } - } - - handleContainerDestroy(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerDestroy?.(containerInfo); - } - } - - handleContainerPause(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerPause?.(containerInfo); - } - } - - handleContainerUnpause(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerUnpause?.(containerInfo); - } - } - - handleContainerRestart(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerRestart?.(containerInfo); - } - } - - handleContainerUpdate(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerUpdate?.(containerInfo); - } - } - - handleContainerRename(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerRename?.(containerInfo); - } - } - - handleContainerHealthStatus(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerHealthStatus?.(containerInfo); - } - } - - handleHostUnreachable(host: string, err: string) { - for (const [, plugin] of this.plugins) { - plugin.onHostUnreachable?.(host, err); - } - } - - handleHostReachableAgain(host: string) { - for (const [, plugin] of this.plugins) { - plugin.onHostReachableAgain?.(host); - } - } - - handleContainerKill(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerKill?.(containerInfo); - } - } - - handleContainerDie(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.handleContainerDie?.(containerInfo); - } - } + private plugins: Map = new Map(); + private failedPlugins: Map = new Map(); + + fail(plugin: Plugin) { + try { + this.failedPlugins.set(plugin.name, plugin); + logger.debug(`Set status to failed for plugin: ${plugin.name}`); + } catch (error) { + logger.error(`Adding failed plugin to list failed: ${error as string}`); + } + } + + register(plugin: Plugin) { + try { + this.plugins.set(plugin.name, plugin); + logger.debug(`Registered plugin: ${plugin.name}`); + } catch (error) { + logger.error( + `Registering plugin ${plugin.name} failed: ${error as string}`, + ); + } + } + + unregister(name: string) { + this.plugins.delete(name); + } + + getPlugins(): PluginInfo[] { + const loadedPlugins = Array.from(this.plugins.values()).map((plugin) => { + logger.debug(`Loaded plugin: ${plugin}`); + const hooks = getHooks(plugin); + return { + name: plugin.name, + status: "active", + usedHooks: hooks, + }; + }); + + const failedPlugins = Array.from(this.failedPlugins.values()).map( + (plugin) => { + const hooks = getHooks(plugin); + + return { + name: plugin.name, + status: "inactive", + usedHooks: hooks, + }; + }, + ); + + return loadedPlugins.concat(failedPlugins); + } + + // Trigger plugin flows: + handleContainerStop(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStop?.(containerInfo); + } + } + + handleContainerStart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStart?.(containerInfo); + } + } + + handleContainerExit(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerExit?.(containerInfo); + } + } + + handleContainerCreate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerCreate?.(containerInfo); + } + } + + handleContainerDestroy(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerDestroy?.(containerInfo); + } + } + + handleContainerPause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerPause?.(containerInfo); + } + } + + handleContainerUnpause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUnpause?.(containerInfo); + } + } + + handleContainerRestart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRestart?.(containerInfo); + } + } + + handleContainerUpdate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUpdate?.(containerInfo); + } + } + + handleContainerRename(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRename?.(containerInfo); + } + } + + handleContainerHealthStatus(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerHealthStatus?.(containerInfo); + } + } + + handleHostUnreachable(host: string, err: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostUnreachable?.(host, err); + } + } + + handleHostReachableAgain(host: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostReachableAgain?.(host); + } + } + + handleContainerKill(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerKill?.(containerInfo); + } + } + + handleContainerDie(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.handleContainerDie?.(containerInfo); + } + } } export const pluginManager = new PluginManager(); diff --git a/src/handlers/config.ts b/src/handlers/config.ts index d87c84a1..96ecadbd 100644 --- a/src/handlers/config.ts +++ b/src/handlers/config.ts @@ -4,182 +4,182 @@ import { backupDir } from "~/core/database/backup"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import type { config } from "../../typings/database"; import type { DockerHost } from "../../typings/docker"; import type { PluginInfo } from "../../typings/plugin"; class apiHandler { - getConfig() { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateConfig(fetching_interval: number, keep_data_for: number) { - try { - dbFunctions.updateConfig(fetching_interval, keep_data_for); - return "Updated DockStatAPI config"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - getPlugins(): PluginInfo[] { - try { - return pluginManager.getPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getPackage() { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json` - ); - - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } - - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async createbackup() { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return backupFilename; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async listBackups() { - try { - const backupFiles = readdirSync(backupDir); - - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.startsWith(".") || - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); - - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async downloadbackup(downloadFile?: string) { - try { - const filename: string = downloadFile || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; - - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } - - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async restoreBackup(file: Bun.FileBlob) { - try { - if (!file) { - throw new Error("No file uploaded"); - } - - if (!(file.name || "").endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } - - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); - - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); - - return "Database restored successfully"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async addHost(host: DockerHost) { - try { - dbFunctions.addDockerHost(host); - return `Added docker host (${host.name} - ${host.hostAddress})`; - } catch (error: unknown) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateHost(host: DockerHost) { - try { - dbFunctions.updateDockerHost(host); - return `Updated docker host (${host.id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async removeHost(id: number) { - try { - dbFunctions.deleteDockerHost(id); - return `Deleted docker host (${id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } + getConfig() { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateConfig(fetching_interval: number, keep_data_for: number) { + try { + dbFunctions.updateConfig(fetching_interval, keep_data_for); + return "Updated DockStatAPI config"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + getPlugins(): PluginInfo[] { + try { + return pluginManager.getPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getPackage() { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, + ); + + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } + + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async createbackup() { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return backupFilename; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async listBackups() { + try { + const backupFiles = readdirSync(backupDir); + + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.startsWith(".") || + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); + + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async downloadbackup(downloadFile?: string) { + try { + const filename: string = downloadFile || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; + + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } + + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async restoreBackup(file: Bun.FileBlob) { + try { + if (!file) { + throw new Error("No file uploaded"); + } + + if (!(file.name || "").endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } + + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return "Database restored successfully"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async addHost(host: DockerHost) { + try { + dbFunctions.addDockerHost(host); + return `Added docker host (${host.name} - ${host.hostAddress})`; + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateHost(host: DockerHost) { + try { + dbFunctions.updateDockerHost(host); + return `Updated docker host (${host.id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async removeHost(id: number) { + try { + dbFunctions.deleteDockerHost(id); + return `Deleted docker host (${id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } } export const ApiHandler = new apiHandler(); diff --git a/src/handlers/docker.ts b/src/handlers/docker.ts index 924c6f35..60f0ea9a 100644 --- a/src/handlers/docker.ts +++ b/src/handlers/docker.ts @@ -4,157 +4,157 @@ import { getDockerClient } from "~/core/docker/client"; import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; import type { - ContainerInfo, - DockerHost, - HostStats, + ContainerInfo, + DockerHost, + HostStats, } from "../../typings/docker"; import type { DockerInfo } from "../../typings/dockerode"; class basicDockerHandler { - async getContainers() { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; - - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - throw new Error(pingError as string); - } - - const hostContainers = await docker.listContainers({ all: true }); - - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - throw new Error(error as string); - } - if (!stats) { - throw new Error("No stats available"); - } - resolve(stats); - }); - } - ); - - containers.push({ - id: containerInfo.Id, - hostId: `${host.id}`, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: stats.cpu_stats.system_cpu_usage, - memoryUsage: stats.memory_stats.usage, - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError - ); - } - }) - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }) - ); - - logger.debug("Fetched all containers across all hosts"); - return { containers }; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getHostStats(id?: number) { - if (!id) { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - - const stats: HostStats[] = []; - - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - stats.push(config); - } - - logger.debug("Fetched all hosts"); - return stats; - } catch (error) { - throw new Error(error as string); - } - } - - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - - const host = findObjectByKey(hosts, "id", Number(id)); - if (!host) { - throw new Error(`Host (${id}) not found`); - } - - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - throw new Error("Failed to retrieve host config"); - } - } + async getContainers() { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; + + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + throw new Error(pingError as string); + } + + const hostContainers = await docker.listContainers({ all: true }); + + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + throw new Error(error as string); + } + if (!stats) { + throw new Error("No stats available"); + } + resolve(stats); + }); + }, + ); + + containers.push({ + id: containerInfo.Id, + hostId: `${host.id}`, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: stats.cpu_stats.system_cpu_usage, + memoryUsage: stats.memory_stats.usage, + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }), + ); + + logger.debug("Fetched all containers across all hosts"); + return { containers }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getHostStats(id?: number) { + if (!id) { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + const stats: HostStats[] = []; + + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + stats.push(config); + } + + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + throw new Error(error as string); + } + } + + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + const host = findObjectByKey(hosts, "id", Number(id)); + if (!host) { + throw new Error(`Host (${id}) not found`); + } + + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + throw new Error("Failed to retrieve host config"); + } + } } export const BasicDockerHandler = new basicDockerHandler(); diff --git a/src/handlers/index.ts b/src/handlers/index.ts index 38fd387c..b0ca70b4 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -8,12 +8,12 @@ import { StackHandler } from "./stacks"; import { CheckHealth } from "./utils"; export const handlers = { - BasicDockerHandler, - ApiHandler, - DatabaseHandler, - StackHandler, - LogHandler, - CheckHealth, - Sockets: Sockets, - Start: setSchedules(), + BasicDockerHandler, + ApiHandler, + DatabaseHandler, + StackHandler, + LogHandler, + CheckHealth, + Sockets: Sockets, + Start: setSchedules(), }; diff --git a/src/handlers/modules/docker-socket.ts b/src/handlers/modules/docker-socket.ts index a6301564..472f9c5c 100644 --- a/src/handlers/modules/docker-socket.ts +++ b/src/handlers/modules/docker-socket.ts @@ -3,140 +3,140 @@ import split2 from "split2"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; import type { DockerStatsEvent } from "../../../typings/docker"; export function createDockerStatsStream(): Readable { - const stream = new Readable({ - objectMode: true, - read() {}, - }); + const stream = new Readable({ + objectMode: true, + read() {}, + }); - const substreams: Array<{ - statsStream: Readable; - splitStream: Transform; - }> = []; + const substreams: Array<{ + statsStream: Readable; + splitStream: Transform; + }> = []; - const cleanup = () => { - for (const { statsStream, splitStream } of substreams) { - try { - statsStream.unpipe(splitStream); - statsStream.destroy(); - splitStream.destroy(); - } catch (error) { - logger.error(`Cleanup error: ${error}`); - } - } - substreams.length = 0; - }; + const cleanup = () => { + for (const { statsStream, splitStream } of substreams) { + try { + statsStream.unpipe(splitStream); + statsStream.destroy(); + splitStream.destroy(); + } catch (error) { + logger.error(`Cleanup error: ${error}`); + } + } + substreams.length = 0; + }; - stream.on("close", cleanup); - stream.on("error", cleanup); + stream.on("close", cleanup); + stream.on("error", cleanup); - (async () => { - try { - const hosts = dbFunctions.getDockerHosts(); - logger.debug(`Retrieved ${hosts.length} docker host(s)`); + (async () => { + try { + const hosts = dbFunctions.getDockerHosts(); + logger.debug(`Retrieved ${hosts.length} docker host(s)`); - for (const host of hosts) { - if (stream.destroyed) break; + for (const host of hosts) { + if (stream.destroyed) break; - try { - const docker = getDockerClient(host); - await docker.ping(); - const containers = await docker.listContainers({ - all: true, - }); + try { + const docker = getDockerClient(host); + await docker.ping(); + const containers = await docker.listContainers({ + all: true, + }); - logger.debug( - `Found ${containers.length} containers on ${host.name} (id: ${host.id})` - ); + logger.debug( + `Found ${containers.length} containers on ${host.name} (id: ${host.id})`, + ); - for (const containerInfo of containers) { - if (stream.destroyed) break; + for (const containerInfo of containers) { + if (stream.destroyed) break; - try { - const container = docker.getContainer(containerInfo.Id); - const statsStream = (await container.stats({ - stream: true, - })) as Readable; - const splitStream = split2(); + try { + const container = docker.getContainer(containerInfo.Id); + const statsStream = (await container.stats({ + stream: true, + })) as Readable; + const splitStream = split2(); - substreams.push({ statsStream, splitStream }); + substreams.push({ statsStream, splitStream }); - statsStream - .on("close", () => splitStream.destroy()) - .pipe(splitStream) - .on("data", (line: string) => { - if (stream.destroyed || !line) return; + statsStream + .on("close", () => splitStream.destroy()) + .pipe(splitStream) + .on("data", (line: string) => { + if (stream.destroyed || !line) return; - try { - const stats = JSON.parse(line); - const event: DockerStatsEvent = { - type: "stats", - id: containerInfo.Id, - hostId: host.id, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats) ?? 0, - memoryUsage: calculateMemoryUsage(stats) ?? 0, - }; - stream.push(event); - } catch (error) { - stream.push({ - type: "error", - hostId: host.id, - containerId: containerInfo.Id, - error: `Parse error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); - } - }) - .on("error", (error: Error) => { - stream.push({ - type: "error", - hostId: host.id, - containerId: containerInfo.Id, - error: `Stream error: ${error.message}`, - }); - }); - } catch (error) { - stream.push({ - type: "error", - hostId: host.id, - containerId: containerInfo.Id, - error: `Container error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); - } - } - } catch (error) { - stream.push({ - type: "error", - hostId: host.id, - error: `Host connection error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); - } - } - } catch (error) { - stream.push({ - type: "error", - error: `Initialization error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); - stream.destroy(); - } - })(); + try { + const stats = JSON.parse(line); + const event: DockerStatsEvent = { + type: "stats", + id: containerInfo.Id, + hostId: host.id, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats) ?? 0, + memoryUsage: calculateMemoryUsage(stats) ?? 0, + }; + stream.push(event); + } catch (error) { + stream.push({ + type: "error", + hostId: host.id, + containerId: containerInfo.Id, + error: `Parse error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + }) + .on("error", (error: Error) => { + stream.push({ + type: "error", + hostId: host.id, + containerId: containerInfo.Id, + error: `Stream error: ${error.message}`, + }); + }); + } catch (error) { + stream.push({ + type: "error", + hostId: host.id, + containerId: containerInfo.Id, + error: `Container error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + } + } catch (error) { + stream.push({ + type: "error", + hostId: host.id, + error: `Host connection error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + } + } catch (error) { + stream.push({ + type: "error", + error: `Initialization error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + stream.destroy(); + } + })(); - return stream; + return stream; } diff --git a/src/handlers/stacks.ts b/src/handlers/stacks.ts index f064bdad..34029875 100644 --- a/src/handlers/stacks.ts +++ b/src/handlers/stacks.ts @@ -1,127 +1,127 @@ import { dbFunctions } from "~/core/database"; import { - deployStack, - getAllStacksStatus, - getStackStatus, - pullStackImages, - removeStack, - restartStack, - startStack, - stopStack, + deployStack, + getAllStacksStatus, + getStackStatus, + pullStackImages, + removeStack, + restartStack, + startStack, + stopStack, } from "~/core/stacks/controller"; import { logger } from "~/core/utils/logger"; import type { stacks_config } from "~/typings/database"; class stackHandler { - async deploy(config: stacks_config) { - try { - await deployStack(config); - logger.info(`Deployed Stack (${config.name})`); - return `Stack ${config.name} deployed successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error deploying stack, please check the server logs for more information`; - } - } - - async start(stackId: number) { - try { - if (!stackId) { - throw new Error("Stack ID needed"); - } - await startStack(stackId); - logger.info(`Started Stack (${stackId})`); - return `Stack ${stackId} started successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error starting stack`; - } - } - - async stop(stackId: number) { - try { - if (!stackId) { - throw new Error("Stack needed"); - } - await stopStack(stackId); - logger.info(`Stopped Stack (${stackId})`); - return `Stack ${stackId} stopped successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error stopping stack`; - } - } - - async restart(stackId: number) { - try { - if (!stackId) { - throw new Error("StackID needed"); - } - await restartStack(stackId); - logger.info(`Restarted Stack (${stackId})`); - return `Stack ${stackId} restarted successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error restarting stack`; - } - } - - async pullImages(stackId: number) { - try { - if (!stackId) { - throw new Error("StackID needed"); - } - await pullStackImages(stackId); - logger.info(`Pulled Stack images (${stackId})`); - return `Images for stack ${stackId} pulled successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error pulling images`; - } - } - - async getStatus(stackId?: number) { - if (stackId) { - const status = await getStackStatus(stackId); - logger.debug( - `Retrieved status for stackId=${stackId}: ${JSON.stringify(status)}` - ); - return status; - } - - logger.debug("Fetching status for all stacks"); - const status = await getAllStacksStatus(); - logger.debug(`Retrieved status for all stacks: ${JSON.stringify(status)}`); - - return status; - } - - listStacks(): stacks_config[] { - try { - const stacks = dbFunctions.getStacks(); - logger.info("Fetched Stacks"); - return stacks; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - throw new Error(`${errorMsg}, Error getting stacks`); - } - } - - async deleteStack(stackId: number) { - try { - await removeStack(stackId); - logger.info(`Deleted Stack ${stackId}`); - return `Stack ${stackId} deleted successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - return `${errorMsg}, Error deleting stack`; - } - } + async deploy(config: stacks_config) { + try { + await deployStack(config); + logger.info(`Deployed Stack (${config.name})`); + return `Stack ${config.name} deployed successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error deploying stack, please check the server logs for more information`; + } + } + + async start(stackId: number) { + try { + if (!stackId) { + throw new Error("Stack ID needed"); + } + await startStack(stackId); + logger.info(`Started Stack (${stackId})`); + return `Stack ${stackId} started successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error starting stack`; + } + } + + async stop(stackId: number) { + try { + if (!stackId) { + throw new Error("Stack needed"); + } + await stopStack(stackId); + logger.info(`Stopped Stack (${stackId})`); + return `Stack ${stackId} stopped successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error stopping stack`; + } + } + + async restart(stackId: number) { + try { + if (!stackId) { + throw new Error("StackID needed"); + } + await restartStack(stackId); + logger.info(`Restarted Stack (${stackId})`); + return `Stack ${stackId} restarted successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error restarting stack`; + } + } + + async pullImages(stackId: number) { + try { + if (!stackId) { + throw new Error("StackID needed"); + } + await pullStackImages(stackId); + logger.info(`Pulled Stack images (${stackId})`); + return `Images for stack ${stackId} pulled successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error pulling images`; + } + } + + async getStatus(stackId?: number) { + if (stackId) { + const status = await getStackStatus(stackId); + logger.debug( + `Retrieved status for stackId=${stackId}: ${JSON.stringify(status)}`, + ); + return status; + } + + logger.debug("Fetching status for all stacks"); + const status = await getAllStacksStatus(); + logger.debug(`Retrieved status for all stacks: ${JSON.stringify(status)}`); + + return status; + } + + listStacks(): stacks_config[] { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks"); + return stacks; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + throw new Error(`${errorMsg}, Error getting stacks`); + } + } + + async deleteStack(stackId: number) { + try { + await removeStack(stackId); + logger.info(`Deleted Stack ${stackId}`); + return `Stack ${stackId} deleted successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return `${errorMsg}, Error deleting stack`; + } + } } export const StackHandler = new stackHandler(); diff --git a/src/handlers/utils.ts b/src/handlers/utils.ts index 206f5424..fd896dea 100644 --- a/src/handlers/utils.ts +++ b/src/handlers/utils.ts @@ -1,6 +1,6 @@ import { logger } from "~/core/utils/logger"; export async function CheckHealth() { - logger.info("Checking health"); - return "healthy"; + logger.info("Checking health"); + return "healthy"; } From b32bf0a1a2dbd68c17a0125e66ad37993754f5a5 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Mon, 30 Jun 2025 20:43:19 +0200 Subject: [PATCH 344/369] chore: some minor patches --- src/handlers/docker.ts | 8 +- src/plugins/example.plugin.ts | 180 +++++++++++++++++----------------- typings | 2 +- 3 files changed, 95 insertions(+), 95 deletions(-) diff --git a/src/handlers/docker.ts b/src/handlers/docker.ts index 924c6f35..54f41858 100644 --- a/src/handlers/docker.ts +++ b/src/handlers/docker.ts @@ -11,7 +11,7 @@ import type { import type { DockerInfo } from "../../typings/dockerode"; class basicDockerHandler { - async getContainers() { + async getContainers(): Promise { try { const hosts = dbFunctions.getDockerHosts() as DockerHost[]; const containers: ContainerInfo[] = []; @@ -48,7 +48,7 @@ class basicDockerHandler { containers.push({ id: containerInfo.Id, - hostId: `${host.id}`, + hostId: host.id, name: containerInfo.Names[0].replace(/^\//, ""), image: containerInfo.Image, status: containerInfo.Status, @@ -76,7 +76,7 @@ class basicDockerHandler { ); logger.debug("Fetched all containers across all hosts"); - return { containers }; + return containers; } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); throw new Error(errMsg); @@ -152,7 +152,7 @@ class basicDockerHandler { logger.debug(`Fetched config for ${host.name}`); return config; } catch (error) { - throw new Error("Failed to retrieve host config"); + throw new Error(`Failed to retrieve host config: ${error}`); } } } diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index 178ea705..ac2b3627 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -1,99 +1,99 @@ import { logger } from "~/core/utils/logger"; -import type { ContainerInfo } from "~/typings/docker"; -import type { Plugin } from "~/typings/plugin"; +import type { ContainerInfo } from "../../typings/docker"; +import type { Plugin } from "../../typings/plugin"; // See https://outline.itsnik.de/s/dockstat/doc/plugin-development-3UBj9gNMKF for more info const ExamplePlugin: Plugin = { - name: "Example Plugin", - version: "1.0.0", - - async onContainerStart(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} started on ${containerInfo.hostId}`, - ); - }, - - async onContainerStop(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} stopped on ${containerInfo.hostId}`, - ); - }, - - async onContainerExit(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} exited on ${containerInfo.hostId}`, - ); - }, - - async onContainerCreate(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} created on ${containerInfo.hostId}`, - ); - }, - - async onContainerDestroy(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}`, - ); - }, - - async onContainerPause(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} pause on ${containerInfo.hostId}`, - ); - }, - - async onContainerUnpause(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} resumed on ${containerInfo.hostId}`, - ); - }, - - async onContainerRestart(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} restarted on ${containerInfo.hostId}`, - ); - }, - - async onContainerUpdate(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} updated on ${containerInfo.hostId}`, - ); - }, - - async onContainerRename(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} renamed on ${containerInfo.hostId}`, - ); - }, - - async onContainerHealthStatus(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} changed status to ${containerInfo.status}`, - ); - }, - - async onHostUnreachable(host: string, err: string) { - logger.info(`Server ${host} unreachable - ${err}`); - }, - - async onHostReachableAgain(host: string) { - logger.info(`Server ${host} reachable`); - }, - - async handleContainerDie(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} died on ${containerInfo.hostId}`, - ); - }, - - async onContainerKill(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} killed on ${containerInfo.hostId}`, - ); - }, + name: "Example Plugin", + version: "1.0.0", + + async onContainerStart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} started on ${containerInfo.hostId}` + ); + }, + + async onContainerStop(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} stopped on ${containerInfo.hostId}` + ); + }, + + async onContainerExit(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} exited on ${containerInfo.hostId}` + ); + }, + + async onContainerCreate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} created on ${containerInfo.hostId}` + ); + }, + + async onContainerDestroy(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}` + ); + }, + + async onContainerPause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} pause on ${containerInfo.hostId}` + ); + }, + + async onContainerUnpause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} resumed on ${containerInfo.hostId}` + ); + }, + + async onContainerRestart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} restarted on ${containerInfo.hostId}` + ); + }, + + async onContainerUpdate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} updated on ${containerInfo.hostId}` + ); + }, + + async onContainerRename(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} renamed on ${containerInfo.hostId}` + ); + }, + + async onContainerHealthStatus(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} changed status to ${containerInfo.status}` + ); + }, + + async onHostUnreachable(host: string, err: string) { + logger.info(`Server ${host} unreachable - ${err}`); + }, + + async onHostReachableAgain(host: string) { + logger.info(`Server ${host} reachable`); + }, + + async handleContainerDie(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} died on ${containerInfo.hostId}` + ); + }, + + async onContainerKill(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} killed on ${containerInfo.hostId}` + ); + }, } satisfies Plugin; export default ExamplePlugin; diff --git a/typings b/typings index 38ef6e0b..e029d99d 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 38ef6e0bb047ff502e76e440b836b214ba1f04fe +Subproject commit e029d99dcfbadf80156532ee57dd5f969c696332 From c24248b8229442e202eff77a470829b8fe4e83a2 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Mon, 30 Jun 2025 20:43:55 +0200 Subject: [PATCH 345/369] Lint fixes --- src/core/docker/client.ts | 52 ++-- src/core/docker/scheduler.ts | 206 ++++++++-------- src/core/plugins/plugin-manager.ts | 320 ++++++++++++------------ src/handlers/config.ts | 340 +++++++++++++------------- src/handlers/docker.ts | 294 +++++++++++----------- src/handlers/index.ts | 16 +- src/handlers/modules/docker-socket.ts | 236 +++++++++--------- src/handlers/stacks.ts | 234 +++++++++--------- src/handlers/utils.ts | 4 +- src/plugins/example.plugin.ts | 176 ++++++------- 10 files changed, 939 insertions(+), 939 deletions(-) diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index cd252d68..35876a6e 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -3,33 +3,33 @@ import { logger } from "~/core/utils/logger"; import type { DockerHost } from "../../../typings/docker"; export const getDockerClient = (host: DockerHost): Docker => { - try { - logger.info(`Setting up host: ${JSON.stringify(host)}`); + try { + logger.info(`Setting up host: ${JSON.stringify(host)}`); - const inputUrl = host.hostAddress.includes("://") - ? host.hostAddress - : `${host.secure ? "https" : "http"}://${host.hostAddress}`; - const parsedUrl = new URL(inputUrl); - const hostAddress = parsedUrl.hostname; - const port = parsedUrl.port - ? Number.parseInt(parsedUrl.port) - : host.secure - ? 2376 - : 2375; + const inputUrl = host.hostAddress.includes("://") + ? host.hostAddress + : `${host.secure ? "https" : "http"}://${host.hostAddress}`; + const parsedUrl = new URL(inputUrl); + const hostAddress = parsedUrl.hostname; + const port = parsedUrl.port + ? Number.parseInt(parsedUrl.port) + : host.secure + ? 2376 + : 2375; - if (Number.isNaN(port) || port < 1 || port > 65535) { - throw new Error("Invalid port number in Docker host URL"); - } + if (Number.isNaN(port) || port < 1 || port > 65535) { + throw new Error("Invalid port number in Docker host URL"); + } - return new Docker({ - protocol: host.secure ? "https" : "http", - host: hostAddress, - port, - version: "v1.41", - // TODO: Add TLS configuration if needed - }); - } catch (error) { - logger.error("Invalid Docker host URL configuration:", error); - throw new Error("Invalid Docker host configuration"); - } + return new Docker({ + protocol: host.secure ? "https" : "http", + host: hostAddress, + port, + version: "v1.41", + // TODO: Add TLS configuration if needed + }); + } catch (error) { + logger.error("Invalid Docker host URL configuration:", error); + throw new Error("Invalid Docker host configuration"); + } }; diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index 3453811d..8754fd66 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -5,120 +5,120 @@ import { logger } from "~/core/utils/logger"; import type { config } from "../../../typings/database"; function convertFromMinToMs(minutes: number): number { - return minutes * 60 * 1000; + return minutes * 60 * 1000; } async function initialRun( - scheduleName: string, - scheduleFunction: Promise | void, - isAsync: boolean + scheduleName: string, + scheduleFunction: Promise | void, + isAsync: boolean, ) { - try { - if (isAsync) { - await scheduleFunction; - } else { - scheduleFunction; - } - logger.info(`Startup run success for: ${scheduleName}`); - } catch (error) { - logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); - } + try { + if (isAsync) { + await scheduleFunction; + } else { + scheduleFunction; + } + logger.info(`Startup run success for: ${scheduleName}`); + } catch (error) { + logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); + } } async function scheduledJob( - name: string, - jobFn: () => Promise, - intervalMs: number + name: string, + jobFn: () => Promise, + intervalMs: number, ) { - while (true) { - const start = Date.now(); - logger.info(`Task Start: ${name}`); - try { - await jobFn(); - logger.info(`Task End: ${name} succeeded.`); - } catch (e) { - logger.error(`Task End: ${name} failed:`, e); - } - const elapsed = Date.now() - start; - const delay = Math.max(0, intervalMs - elapsed); - await new Promise((r) => setTimeout(r, delay)); - } + while (true) { + const start = Date.now(); + logger.info(`Task Start: ${name}`); + try { + await jobFn(); + logger.info(`Task End: ${name} succeeded.`); + } catch (e) { + logger.error(`Task End: ${name} failed:`, e); + } + const elapsed = Date.now() - start; + const delay = Math.max(0, intervalMs - elapsed); + await new Promise((r) => setTimeout(r, delay)); + } } async function setSchedules() { - logger.info("Starting DockStatAPI"); - try { - const rawConfigData: unknown[] = dbFunctions.getConfig(); - const configData = rawConfigData[0]; - - if ( - !configData || - typeof (configData as config).keep_data_for !== "number" || - typeof (configData as config).fetching_interval !== "number" - ) { - logger.error("Invalid configuration data:", configData); - throw new Error("Invalid configuration data"); - } - - const { keep_data_for, fetching_interval } = configData as config; - - if (keep_data_for === undefined) { - const errMsg = "keep_data_for is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } - - if (fetching_interval === undefined) { - const errMsg = "fetching_interval is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } - - logger.info( - `Scheduling: Fetching container statistics every ${fetching_interval} minutes` - ); - - logger.info( - `Scheduling: Updating host statistics every ${fetching_interval} minutes` - ); - - logger.info( - `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days` - ); - - // Schedule container data fetching - await initialRun("storeContainerData", storeContainerData(), true); - scheduledJob( - "storeContainerData", - storeContainerData, - convertFromMinToMs(fetching_interval) - ); - - // Schedule Host statistics updates - await initialRun("storeHostData", storeHostData(), true); - scheduledJob( - "storeHostData", - storeHostData, - convertFromMinToMs(fetching_interval) - ); - - // Schedule database cleanup - await initialRun( - "dbFunctions.deleteOldData", - dbFunctions.deleteOldData(keep_data_for), - false - ); - scheduledJob( - "cleanupOldData", - () => Promise.resolve(dbFunctions.deleteOldData(keep_data_for)), - convertFromMinToMs(60) - ); - - logger.info("Schedules have been set successfully."); - } catch (error) { - logger.error("Error setting schedules:", error); - throw error; - } + logger.info("Starting DockStatAPI"); + try { + const rawConfigData: unknown[] = dbFunctions.getConfig(); + const configData = rawConfigData[0]; + + if ( + !configData || + typeof (configData as config).keep_data_for !== "number" || + typeof (configData as config).fetching_interval !== "number" + ) { + logger.error("Invalid configuration data:", configData); + throw new Error("Invalid configuration data"); + } + + const { keep_data_for, fetching_interval } = configData as config; + + if (keep_data_for === undefined) { + const errMsg = "keep_data_for is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + if (fetching_interval === undefined) { + const errMsg = "fetching_interval is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + logger.info( + `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, + ); + + logger.info( + `Scheduling: Updating host statistics every ${fetching_interval} minutes`, + ); + + logger.info( + `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, + ); + + // Schedule container data fetching + await initialRun("storeContainerData", storeContainerData(), true); + scheduledJob( + "storeContainerData", + storeContainerData, + convertFromMinToMs(fetching_interval), + ); + + // Schedule Host statistics updates + await initialRun("storeHostData", storeHostData(), true); + scheduledJob( + "storeHostData", + storeHostData, + convertFromMinToMs(fetching_interval), + ); + + // Schedule database cleanup + await initialRun( + "dbFunctions.deleteOldData", + dbFunctions.deleteOldData(keep_data_for), + false, + ); + scheduledJob( + "cleanupOldData", + () => Promise.resolve(dbFunctions.deleteOldData(keep_data_for)), + convertFromMinToMs(60), + ); + + logger.info("Schedules have been set successfully."); + } catch (error) { + logger.error("Error setting schedules:", error); + throw error; + } } export { setSchedules }; diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index 383acfdb..e0bf121e 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -4,169 +4,169 @@ import type { Plugin, PluginInfo } from "../../../typings/plugin"; import { logger } from "../utils/logger"; function getHooks(plugin: Plugin) { - return { - onContainerStart: !!plugin.onContainerStart, - onContainerStop: !!plugin.onContainerStop, - onContainerExit: !!plugin.onContainerExit, - onContainerCreate: !!plugin.onContainerCreate, - onContainerKill: !!plugin.onContainerKill, - handleContainerDie: !!plugin.handleContainerDie, - onContainerDestroy: !!plugin.onContainerDestroy, - onContainerPause: !!plugin.onContainerPause, - onContainerUnpause: !!plugin.onContainerUnpause, - onContainerRestart: !!plugin.onContainerRestart, - onContainerUpdate: !!plugin.onContainerUpdate, - onContainerRename: !!plugin.onContainerRename, - onContainerHealthStatus: !!plugin.onContainerHealthStatus, - onHostUnreachable: !!plugin.onHostUnreachable, - onHostReachableAgain: !!plugin.onHostReachableAgain, - }; + return { + onContainerStart: !!plugin.onContainerStart, + onContainerStop: !!plugin.onContainerStop, + onContainerExit: !!plugin.onContainerExit, + onContainerCreate: !!plugin.onContainerCreate, + onContainerKill: !!plugin.onContainerKill, + handleContainerDie: !!plugin.handleContainerDie, + onContainerDestroy: !!plugin.onContainerDestroy, + onContainerPause: !!plugin.onContainerPause, + onContainerUnpause: !!plugin.onContainerUnpause, + onContainerRestart: !!plugin.onContainerRestart, + onContainerUpdate: !!plugin.onContainerUpdate, + onContainerRename: !!plugin.onContainerRename, + onContainerHealthStatus: !!plugin.onContainerHealthStatus, + onHostUnreachable: !!plugin.onHostUnreachable, + onHostReachableAgain: !!plugin.onHostReachableAgain, + }; } class PluginManager extends EventEmitter { - private plugins: Map = new Map(); - private failedPlugins: Map = new Map(); - - fail(plugin: Plugin) { - try { - this.failedPlugins.set(plugin.name, plugin); - logger.debug(`Set status to failed for plugin: ${plugin.name}`); - } catch (error) { - logger.error(`Adding failed plugin to list failed: ${error as string}`); - } - } - - register(plugin: Plugin) { - try { - this.plugins.set(plugin.name, plugin); - logger.debug(`Registered plugin: ${plugin.name}`); - } catch (error) { - logger.error( - `Registering plugin ${plugin.name} failed: ${error as string}` - ); - } - } - - unregister(name: string) { - this.plugins.delete(name); - } - - getPlugins(): PluginInfo[] { - const loadedPlugins = Array.from(this.plugins.values()).map((plugin) => { - logger.debug(`Loaded plugin: ${plugin}`); - const hooks = getHooks(plugin); - return { - name: plugin.name, - status: "active", - usedHooks: hooks, - }; - }); - - const failedPlugins = Array.from(this.failedPlugins.values()).map( - (plugin) => { - const hooks = getHooks(plugin); - - return { - name: plugin.name, - status: "inactive", - usedHooks: hooks, - }; - } - ); - - return loadedPlugins.concat(failedPlugins); - } - - // Trigger plugin flows: - handleContainerStop(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerStop?.(containerInfo); - } - } - - handleContainerStart(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerStart?.(containerInfo); - } - } - - handleContainerExit(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerExit?.(containerInfo); - } - } - - handleContainerCreate(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerCreate?.(containerInfo); - } - } - - handleContainerDestroy(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerDestroy?.(containerInfo); - } - } - - handleContainerPause(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerPause?.(containerInfo); - } - } - - handleContainerUnpause(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerUnpause?.(containerInfo); - } - } - - handleContainerRestart(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerRestart?.(containerInfo); - } - } - - handleContainerUpdate(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerUpdate?.(containerInfo); - } - } - - handleContainerRename(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerRename?.(containerInfo); - } - } - - handleContainerHealthStatus(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerHealthStatus?.(containerInfo); - } - } - - handleHostUnreachable(host: string, err: string) { - for (const [, plugin] of this.plugins) { - plugin.onHostUnreachable?.(host, err); - } - } - - handleHostReachableAgain(host: string) { - for (const [, plugin] of this.plugins) { - plugin.onHostReachableAgain?.(host); - } - } - - handleContainerKill(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.onContainerKill?.(containerInfo); - } - } - - handleContainerDie(containerInfo: ContainerInfo) { - for (const [, plugin] of this.plugins) { - plugin.handleContainerDie?.(containerInfo); - } - } + private plugins: Map = new Map(); + private failedPlugins: Map = new Map(); + + fail(plugin: Plugin) { + try { + this.failedPlugins.set(plugin.name, plugin); + logger.debug(`Set status to failed for plugin: ${plugin.name}`); + } catch (error) { + logger.error(`Adding failed plugin to list failed: ${error as string}`); + } + } + + register(plugin: Plugin) { + try { + this.plugins.set(plugin.name, plugin); + logger.debug(`Registered plugin: ${plugin.name}`); + } catch (error) { + logger.error( + `Registering plugin ${plugin.name} failed: ${error as string}`, + ); + } + } + + unregister(name: string) { + this.plugins.delete(name); + } + + getPlugins(): PluginInfo[] { + const loadedPlugins = Array.from(this.plugins.values()).map((plugin) => { + logger.debug(`Loaded plugin: ${plugin}`); + const hooks = getHooks(plugin); + return { + name: plugin.name, + status: "active", + usedHooks: hooks, + }; + }); + + const failedPlugins = Array.from(this.failedPlugins.values()).map( + (plugin) => { + const hooks = getHooks(plugin); + + return { + name: plugin.name, + status: "inactive", + usedHooks: hooks, + }; + }, + ); + + return loadedPlugins.concat(failedPlugins); + } + + // Trigger plugin flows: + handleContainerStop(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStop?.(containerInfo); + } + } + + handleContainerStart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerStart?.(containerInfo); + } + } + + handleContainerExit(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerExit?.(containerInfo); + } + } + + handleContainerCreate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerCreate?.(containerInfo); + } + } + + handleContainerDestroy(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerDestroy?.(containerInfo); + } + } + + handleContainerPause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerPause?.(containerInfo); + } + } + + handleContainerUnpause(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUnpause?.(containerInfo); + } + } + + handleContainerRestart(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRestart?.(containerInfo); + } + } + + handleContainerUpdate(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerUpdate?.(containerInfo); + } + } + + handleContainerRename(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerRename?.(containerInfo); + } + } + + handleContainerHealthStatus(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerHealthStatus?.(containerInfo); + } + } + + handleHostUnreachable(host: string, err: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostUnreachable?.(host, err); + } + } + + handleHostReachableAgain(host: string) { + for (const [, plugin] of this.plugins) { + plugin.onHostReachableAgain?.(host); + } + } + + handleContainerKill(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.onContainerKill?.(containerInfo); + } + } + + handleContainerDie(containerInfo: ContainerInfo) { + for (const [, plugin] of this.plugins) { + plugin.handleContainerDie?.(containerInfo); + } + } } export const pluginManager = new PluginManager(); diff --git a/src/handlers/config.ts b/src/handlers/config.ts index d87c84a1..96ecadbd 100644 --- a/src/handlers/config.ts +++ b/src/handlers/config.ts @@ -4,182 +4,182 @@ import { backupDir } from "~/core/database/backup"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import type { config } from "../../typings/database"; import type { DockerHost } from "../../typings/docker"; import type { PluginInfo } from "../../typings/plugin"; class apiHandler { - getConfig() { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateConfig(fetching_interval: number, keep_data_for: number) { - try { - dbFunctions.updateConfig(fetching_interval, keep_data_for); - return "Updated DockStatAPI config"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - getPlugins(): PluginInfo[] { - try { - return pluginManager.getPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getPackage() { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json` - ); - - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } - - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async createbackup() { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return backupFilename; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async listBackups() { - try { - const backupFiles = readdirSync(backupDir); - - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.startsWith(".") || - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); - - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async downloadbackup(downloadFile?: string) { - try { - const filename: string = downloadFile || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; - - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } - - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async restoreBackup(file: Bun.FileBlob) { - try { - if (!file) { - throw new Error("No file uploaded"); - } - - if (!(file.name || "").endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } - - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); - - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); - - return "Database restored successfully"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async addHost(host: DockerHost) { - try { - dbFunctions.addDockerHost(host); - return `Added docker host (${host.name} - ${host.hostAddress})`; - } catch (error: unknown) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateHost(host: DockerHost) { - try { - dbFunctions.updateDockerHost(host); - return `Updated docker host (${host.id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async removeHost(id: number) { - try { - dbFunctions.deleteDockerHost(id); - return `Deleted docker host (${id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } + getConfig() { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateConfig(fetching_interval: number, keep_data_for: number) { + try { + dbFunctions.updateConfig(fetching_interval, keep_data_for); + return "Updated DockStatAPI config"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + getPlugins(): PluginInfo[] { + try { + return pluginManager.getPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getPackage() { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, + ); + + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } + + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async createbackup() { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return backupFilename; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async listBackups() { + try { + const backupFiles = readdirSync(backupDir); + + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.startsWith(".") || + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); + + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async downloadbackup(downloadFile?: string) { + try { + const filename: string = downloadFile || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; + + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } + + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async restoreBackup(file: Bun.FileBlob) { + try { + if (!file) { + throw new Error("No file uploaded"); + } + + if (!(file.name || "").endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } + + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return "Database restored successfully"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async addHost(host: DockerHost) { + try { + dbFunctions.addDockerHost(host); + return `Added docker host (${host.name} - ${host.hostAddress})`; + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateHost(host: DockerHost) { + try { + dbFunctions.updateDockerHost(host); + return `Updated docker host (${host.id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async removeHost(id: number) { + try { + dbFunctions.deleteDockerHost(id); + return `Deleted docker host (${id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } } export const ApiHandler = new apiHandler(); diff --git a/src/handlers/docker.ts b/src/handlers/docker.ts index 54f41858..02e10dc6 100644 --- a/src/handlers/docker.ts +++ b/src/handlers/docker.ts @@ -4,157 +4,157 @@ import { getDockerClient } from "~/core/docker/client"; import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; import type { - ContainerInfo, - DockerHost, - HostStats, + ContainerInfo, + DockerHost, + HostStats, } from "../../typings/docker"; import type { DockerInfo } from "../../typings/dockerode"; class basicDockerHandler { - async getContainers(): Promise { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const containers: ContainerInfo[] = []; - - await Promise.all( - hosts.map(async (host) => { - try { - const docker = getDockerClient(host); - try { - await docker.ping(); - } catch (pingError) { - throw new Error(pingError as string); - } - - const hostContainers = await docker.listContainers({ all: true }); - - await Promise.all( - hostContainers.map(async (containerInfo) => { - try { - const container = docker.getContainer(containerInfo.Id); - const stats = await new Promise( - (resolve) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - throw new Error(error as string); - } - if (!stats) { - throw new Error("No stats available"); - } - resolve(stats); - }); - } - ); - - containers.push({ - id: containerInfo.Id, - hostId: host.id, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: stats.cpu_stats.system_cpu_usage, - memoryUsage: stats.memory_stats.usage, - stats: stats, - info: containerInfo, - }); - } catch (containerError) { - logger.error( - "Error fetching container stats,", - containerError - ); - } - }) - ); - logger.debug(`Fetched stats for ${host.name}`); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - }) - ); - - logger.debug("Fetched all containers across all hosts"); - return containers; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getHostStats(id?: number) { - if (!id) { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - - const stats: HostStats[] = []; - - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - stats.push(config); - } - - logger.debug("Fetched all hosts"); - return stats; - } catch (error) { - throw new Error(error as string); - } - } - - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - - const host = findObjectByKey(hosts, "id", Number(id)); - if (!host) { - throw new Error(`Host (${id}) not found`); - } - - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - logger.debug(`Fetched config for ${host.name}`); - return config; - } catch (error) { - throw new Error(`Failed to retrieve host config: ${error}`); - } - } + async getContainers(): Promise { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + const containers: ContainerInfo[] = []; + + await Promise.all( + hosts.map(async (host) => { + try { + const docker = getDockerClient(host); + try { + await docker.ping(); + } catch (pingError) { + throw new Error(pingError as string); + } + + const hostContainers = await docker.listContainers({ all: true }); + + await Promise.all( + hostContainers.map(async (containerInfo) => { + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await new Promise( + (resolve) => { + container.stats({ stream: false }, (error, stats) => { + if (error) { + throw new Error(error as string); + } + if (!stats) { + throw new Error("No stats available"); + } + resolve(stats); + }); + }, + ); + + containers.push({ + id: containerInfo.Id, + hostId: host.id, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: stats.cpu_stats.system_cpu_usage, + memoryUsage: stats.memory_stats.usage, + stats: stats, + info: containerInfo, + }); + } catch (containerError) { + logger.error( + "Error fetching container stats,", + containerError, + ); + } + }), + ); + logger.debug(`Fetched stats for ${host.name}`); + } catch (error) { + const errMsg = + error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + }), + ); + + logger.debug("Fetched all containers across all hosts"); + return containers; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getHostStats(id?: number) { + if (!id) { + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + const stats: HostStats[] = []; + + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + stats.push(config); + } + + logger.debug("Fetched all hosts"); + return stats; + } catch (error) { + throw new Error(error as string); + } + } + + try { + const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + + const host = findObjectByKey(hosts, "id", Number(id)); + if (!host) { + throw new Error(`Host (${id}) not found`); + } + + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + logger.debug(`Fetched config for ${host.name}`); + return config; + } catch (error) { + throw new Error(`Failed to retrieve host config: ${error}`); + } + } } export const BasicDockerHandler = new basicDockerHandler(); diff --git a/src/handlers/index.ts b/src/handlers/index.ts index 38fd387c..b0ca70b4 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -8,12 +8,12 @@ import { StackHandler } from "./stacks"; import { CheckHealth } from "./utils"; export const handlers = { - BasicDockerHandler, - ApiHandler, - DatabaseHandler, - StackHandler, - LogHandler, - CheckHealth, - Sockets: Sockets, - Start: setSchedules(), + BasicDockerHandler, + ApiHandler, + DatabaseHandler, + StackHandler, + LogHandler, + CheckHealth, + Sockets: Sockets, + Start: setSchedules(), }; diff --git a/src/handlers/modules/docker-socket.ts b/src/handlers/modules/docker-socket.ts index a6301564..472f9c5c 100644 --- a/src/handlers/modules/docker-socket.ts +++ b/src/handlers/modules/docker-socket.ts @@ -3,140 +3,140 @@ import split2 from "split2"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; import type { DockerStatsEvent } from "../../../typings/docker"; export function createDockerStatsStream(): Readable { - const stream = new Readable({ - objectMode: true, - read() {}, - }); + const stream = new Readable({ + objectMode: true, + read() {}, + }); - const substreams: Array<{ - statsStream: Readable; - splitStream: Transform; - }> = []; + const substreams: Array<{ + statsStream: Readable; + splitStream: Transform; + }> = []; - const cleanup = () => { - for (const { statsStream, splitStream } of substreams) { - try { - statsStream.unpipe(splitStream); - statsStream.destroy(); - splitStream.destroy(); - } catch (error) { - logger.error(`Cleanup error: ${error}`); - } - } - substreams.length = 0; - }; + const cleanup = () => { + for (const { statsStream, splitStream } of substreams) { + try { + statsStream.unpipe(splitStream); + statsStream.destroy(); + splitStream.destroy(); + } catch (error) { + logger.error(`Cleanup error: ${error}`); + } + } + substreams.length = 0; + }; - stream.on("close", cleanup); - stream.on("error", cleanup); + stream.on("close", cleanup); + stream.on("error", cleanup); - (async () => { - try { - const hosts = dbFunctions.getDockerHosts(); - logger.debug(`Retrieved ${hosts.length} docker host(s)`); + (async () => { + try { + const hosts = dbFunctions.getDockerHosts(); + logger.debug(`Retrieved ${hosts.length} docker host(s)`); - for (const host of hosts) { - if (stream.destroyed) break; + for (const host of hosts) { + if (stream.destroyed) break; - try { - const docker = getDockerClient(host); - await docker.ping(); - const containers = await docker.listContainers({ - all: true, - }); + try { + const docker = getDockerClient(host); + await docker.ping(); + const containers = await docker.listContainers({ + all: true, + }); - logger.debug( - `Found ${containers.length} containers on ${host.name} (id: ${host.id})` - ); + logger.debug( + `Found ${containers.length} containers on ${host.name} (id: ${host.id})`, + ); - for (const containerInfo of containers) { - if (stream.destroyed) break; + for (const containerInfo of containers) { + if (stream.destroyed) break; - try { - const container = docker.getContainer(containerInfo.Id); - const statsStream = (await container.stats({ - stream: true, - })) as Readable; - const splitStream = split2(); + try { + const container = docker.getContainer(containerInfo.Id); + const statsStream = (await container.stats({ + stream: true, + })) as Readable; + const splitStream = split2(); - substreams.push({ statsStream, splitStream }); + substreams.push({ statsStream, splitStream }); - statsStream - .on("close", () => splitStream.destroy()) - .pipe(splitStream) - .on("data", (line: string) => { - if (stream.destroyed || !line) return; + statsStream + .on("close", () => splitStream.destroy()) + .pipe(splitStream) + .on("data", (line: string) => { + if (stream.destroyed || !line) return; - try { - const stats = JSON.parse(line); - const event: DockerStatsEvent = { - type: "stats", - id: containerInfo.Id, - hostId: host.id, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats) ?? 0, - memoryUsage: calculateMemoryUsage(stats) ?? 0, - }; - stream.push(event); - } catch (error) { - stream.push({ - type: "error", - hostId: host.id, - containerId: containerInfo.Id, - error: `Parse error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); - } - }) - .on("error", (error: Error) => { - stream.push({ - type: "error", - hostId: host.id, - containerId: containerInfo.Id, - error: `Stream error: ${error.message}`, - }); - }); - } catch (error) { - stream.push({ - type: "error", - hostId: host.id, - containerId: containerInfo.Id, - error: `Container error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); - } - } - } catch (error) { - stream.push({ - type: "error", - hostId: host.id, - error: `Host connection error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); - } - } - } catch (error) { - stream.push({ - type: "error", - error: `Initialization error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); - stream.destroy(); - } - })(); + try { + const stats = JSON.parse(line); + const event: DockerStatsEvent = { + type: "stats", + id: containerInfo.Id, + hostId: host.id, + name: containerInfo.Names[0].replace(/^\//, ""), + image: containerInfo.Image, + status: containerInfo.Status, + state: containerInfo.State, + cpuUsage: calculateCpuPercent(stats) ?? 0, + memoryUsage: calculateMemoryUsage(stats) ?? 0, + }; + stream.push(event); + } catch (error) { + stream.push({ + type: "error", + hostId: host.id, + containerId: containerInfo.Id, + error: `Parse error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + }) + .on("error", (error: Error) => { + stream.push({ + type: "error", + hostId: host.id, + containerId: containerInfo.Id, + error: `Stream error: ${error.message}`, + }); + }); + } catch (error) { + stream.push({ + type: "error", + hostId: host.id, + containerId: containerInfo.Id, + error: `Container error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + } + } catch (error) { + stream.push({ + type: "error", + hostId: host.id, + error: `Host connection error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + } + } + } catch (error) { + stream.push({ + type: "error", + error: `Initialization error: ${ + error instanceof Error ? error.message : String(error) + }`, + }); + stream.destroy(); + } + })(); - return stream; + return stream; } diff --git a/src/handlers/stacks.ts b/src/handlers/stacks.ts index f064bdad..34029875 100644 --- a/src/handlers/stacks.ts +++ b/src/handlers/stacks.ts @@ -1,127 +1,127 @@ import { dbFunctions } from "~/core/database"; import { - deployStack, - getAllStacksStatus, - getStackStatus, - pullStackImages, - removeStack, - restartStack, - startStack, - stopStack, + deployStack, + getAllStacksStatus, + getStackStatus, + pullStackImages, + removeStack, + restartStack, + startStack, + stopStack, } from "~/core/stacks/controller"; import { logger } from "~/core/utils/logger"; import type { stacks_config } from "~/typings/database"; class stackHandler { - async deploy(config: stacks_config) { - try { - await deployStack(config); - logger.info(`Deployed Stack (${config.name})`); - return `Stack ${config.name} deployed successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error deploying stack, please check the server logs for more information`; - } - } - - async start(stackId: number) { - try { - if (!stackId) { - throw new Error("Stack ID needed"); - } - await startStack(stackId); - logger.info(`Started Stack (${stackId})`); - return `Stack ${stackId} started successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error starting stack`; - } - } - - async stop(stackId: number) { - try { - if (!stackId) { - throw new Error("Stack needed"); - } - await stopStack(stackId); - logger.info(`Stopped Stack (${stackId})`); - return `Stack ${stackId} stopped successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error stopping stack`; - } - } - - async restart(stackId: number) { - try { - if (!stackId) { - throw new Error("StackID needed"); - } - await restartStack(stackId); - logger.info(`Restarted Stack (${stackId})`); - return `Stack ${stackId} restarted successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error restarting stack`; - } - } - - async pullImages(stackId: number) { - try { - if (!stackId) { - throw new Error("StackID needed"); - } - await pullStackImages(stackId); - logger.info(`Pulled Stack images (${stackId})`); - return `Images for stack ${stackId} pulled successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - - return `${errorMsg}, Error pulling images`; - } - } - - async getStatus(stackId?: number) { - if (stackId) { - const status = await getStackStatus(stackId); - logger.debug( - `Retrieved status for stackId=${stackId}: ${JSON.stringify(status)}` - ); - return status; - } - - logger.debug("Fetching status for all stacks"); - const status = await getAllStacksStatus(); - logger.debug(`Retrieved status for all stacks: ${JSON.stringify(status)}`); - - return status; - } - - listStacks(): stacks_config[] { - try { - const stacks = dbFunctions.getStacks(); - logger.info("Fetched Stacks"); - return stacks; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - throw new Error(`${errorMsg}, Error getting stacks`); - } - } - - async deleteStack(stackId: number) { - try { - await removeStack(stackId); - logger.info(`Deleted Stack ${stackId}`); - return `Stack ${stackId} deleted successfully`; - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - return `${errorMsg}, Error deleting stack`; - } - } + async deploy(config: stacks_config) { + try { + await deployStack(config); + logger.info(`Deployed Stack (${config.name})`); + return `Stack ${config.name} deployed successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error deploying stack, please check the server logs for more information`; + } + } + + async start(stackId: number) { + try { + if (!stackId) { + throw new Error("Stack ID needed"); + } + await startStack(stackId); + logger.info(`Started Stack (${stackId})`); + return `Stack ${stackId} started successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error starting stack`; + } + } + + async stop(stackId: number) { + try { + if (!stackId) { + throw new Error("Stack needed"); + } + await stopStack(stackId); + logger.info(`Stopped Stack (${stackId})`); + return `Stack ${stackId} stopped successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error stopping stack`; + } + } + + async restart(stackId: number) { + try { + if (!stackId) { + throw new Error("StackID needed"); + } + await restartStack(stackId); + logger.info(`Restarted Stack (${stackId})`); + return `Stack ${stackId} restarted successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error restarting stack`; + } + } + + async pullImages(stackId: number) { + try { + if (!stackId) { + throw new Error("StackID needed"); + } + await pullStackImages(stackId); + logger.info(`Pulled Stack images (${stackId})`); + return `Images for stack ${stackId} pulled successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + return `${errorMsg}, Error pulling images`; + } + } + + async getStatus(stackId?: number) { + if (stackId) { + const status = await getStackStatus(stackId); + logger.debug( + `Retrieved status for stackId=${stackId}: ${JSON.stringify(status)}`, + ); + return status; + } + + logger.debug("Fetching status for all stacks"); + const status = await getAllStacksStatus(); + logger.debug(`Retrieved status for all stacks: ${JSON.stringify(status)}`); + + return status; + } + + listStacks(): stacks_config[] { + try { + const stacks = dbFunctions.getStacks(); + logger.info("Fetched Stacks"); + return stacks; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + throw new Error(`${errorMsg}, Error getting stacks`); + } + } + + async deleteStack(stackId: number) { + try { + await removeStack(stackId); + logger.info(`Deleted Stack ${stackId}`); + return `Stack ${stackId} deleted successfully`; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return `${errorMsg}, Error deleting stack`; + } + } } export const StackHandler = new stackHandler(); diff --git a/src/handlers/utils.ts b/src/handlers/utils.ts index 206f5424..fd896dea 100644 --- a/src/handlers/utils.ts +++ b/src/handlers/utils.ts @@ -1,6 +1,6 @@ import { logger } from "~/core/utils/logger"; export async function CheckHealth() { - logger.info("Checking health"); - return "healthy"; + logger.info("Checking health"); + return "healthy"; } diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index ac2b3627..38110e06 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -6,94 +6,94 @@ import type { Plugin } from "../../typings/plugin"; // See https://outline.itsnik.de/s/dockstat/doc/plugin-development-3UBj9gNMKF for more info const ExamplePlugin: Plugin = { - name: "Example Plugin", - version: "1.0.0", - - async onContainerStart(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} started on ${containerInfo.hostId}` - ); - }, - - async onContainerStop(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} stopped on ${containerInfo.hostId}` - ); - }, - - async onContainerExit(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} exited on ${containerInfo.hostId}` - ); - }, - - async onContainerCreate(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} created on ${containerInfo.hostId}` - ); - }, - - async onContainerDestroy(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}` - ); - }, - - async onContainerPause(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} pause on ${containerInfo.hostId}` - ); - }, - - async onContainerUnpause(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} resumed on ${containerInfo.hostId}` - ); - }, - - async onContainerRestart(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} restarted on ${containerInfo.hostId}` - ); - }, - - async onContainerUpdate(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} updated on ${containerInfo.hostId}` - ); - }, - - async onContainerRename(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} renamed on ${containerInfo.hostId}` - ); - }, - - async onContainerHealthStatus(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} changed status to ${containerInfo.status}` - ); - }, - - async onHostUnreachable(host: string, err: string) { - logger.info(`Server ${host} unreachable - ${err}`); - }, - - async onHostReachableAgain(host: string) { - logger.info(`Server ${host} reachable`); - }, - - async handleContainerDie(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} died on ${containerInfo.hostId}` - ); - }, - - async onContainerKill(containerInfo: ContainerInfo) { - logger.info( - `Container ${containerInfo.name} killed on ${containerInfo.hostId}` - ); - }, + name: "Example Plugin", + version: "1.0.0", + + async onContainerStart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} started on ${containerInfo.hostId}`, + ); + }, + + async onContainerStop(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} stopped on ${containerInfo.hostId}`, + ); + }, + + async onContainerExit(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} exited on ${containerInfo.hostId}`, + ); + }, + + async onContainerCreate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} created on ${containerInfo.hostId}`, + ); + }, + + async onContainerDestroy(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} destroyed on ${containerInfo.hostId}`, + ); + }, + + async onContainerPause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} pause on ${containerInfo.hostId}`, + ); + }, + + async onContainerUnpause(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} resumed on ${containerInfo.hostId}`, + ); + }, + + async onContainerRestart(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} restarted on ${containerInfo.hostId}`, + ); + }, + + async onContainerUpdate(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} updated on ${containerInfo.hostId}`, + ); + }, + + async onContainerRename(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} renamed on ${containerInfo.hostId}`, + ); + }, + + async onContainerHealthStatus(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} changed status to ${containerInfo.status}`, + ); + }, + + async onHostUnreachable(host: string, err: string) { + logger.info(`Server ${host} unreachable - ${err}`); + }, + + async onHostReachableAgain(host: string) { + logger.info(`Server ${host} reachable`); + }, + + async handleContainerDie(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} died on ${containerInfo.hostId}`, + ); + }, + + async onContainerKill(containerInfo: ContainerInfo) { + logger.info( + `Container ${containerInfo.name} killed on ${containerInfo.hostId}`, + ); + }, } satisfies Plugin; export default ExamplePlugin; From fd4e2193d817e15b49fe9b1ceefb8366fc5c7e9c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Mon, 30 Jun 2025 21:34:59 +0200 Subject: [PATCH 346/369] chore: type fixes --- package.json | 2 +- src/core/database/containerStats.ts | 41 +++++++----------- src/core/docker/client.ts | 2 +- src/core/docker/monitor.ts | 2 +- src/core/docker/scheduler.ts | 2 +- src/core/docker/store-container-stats.ts | 23 +++++----- src/core/plugins/loader.ts | 2 +- src/core/plugins/plugin-manager.ts | 42 ++++++++++--------- src/core/stacks/controller.ts | 6 +++ src/core/stacks/operations/runStackCommand.ts | 4 +- src/core/utils/logger.ts | 6 +-- src/core/utils/package-json.ts | 2 +- src/handlers/config.ts | 6 +-- src/handlers/docker.ts | 8 +--- src/handlers/modules/docker-socket.ts | 2 +- src/plugins/example.plugin.ts | 4 +- src/typings | 1 - tsconfig.json | 5 ++- typings | 2 +- 19 files changed, 81 insertions(+), 81 deletions(-) delete mode 160000 src/typings diff --git a/package.json b/package.json index 0a9529cf..f2e318b5 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@biomejs/biome": "1.9.4", "@its_4_nik/gitai": "^1.1.14", "@types/bun": "latest", - "@types/dockerode": "^3.3.41", + "@types/dockerode": "^3.3.42", "@types/js-yaml": "^4.0.9", "@types/node": "^22.15.32", "@types/split2": "^4.2.3", diff --git a/src/core/database/containerStats.ts b/src/core/database/containerStats.ts index a50ea4c2..a8466701 100644 --- a/src/core/database/containerStats.ts +++ b/src/core/database/containerStats.ts @@ -1,4 +1,4 @@ -import type { containerStatistics } from "~/typings/database"; +import type { container_stats } from "~/typings/database"; import { db } from "./database"; import { executeDbOperation } from "./helper"; @@ -9,35 +9,26 @@ const insert = db.prepare(` const get = db.prepare("SELECT * FROM container_stats"); -export function addContainerStats( - id: string, - hostId: string, - name: string, - image: string, - status: string, - state: string, - cpu_usage: number, - memory_usage: number, -) { +export function addContainerStats(stats: container_stats) { return executeDbOperation( "Add Container Stats", () => insert.run( - id, - hostId, - name, - image, - status, - state, - cpu_usage, - memory_usage, + stats.id, + stats.hostId, + stats.name, + stats.image, + stats.status, + stats.state, + stats.cpu_usage, + stats.memory_usage, ), () => { if ( - typeof id !== "string" || - typeof hostId !== "string" || - typeof cpu_usage !== "number" || - typeof memory_usage !== "number" + typeof stats.id !== "string" || + typeof stats.hostId !== "number" || + typeof stats.cpu_usage !== "number" || + typeof stats.memory_usage !== "number" ) { throw new TypeError("Invalid container stats parameters"); } @@ -45,8 +36,8 @@ export function addContainerStats( ); } -export function getContainerStats(): containerStatistics[] { +export function getContainerStats(): container_stats[] { return executeDbOperation("Get Container Stats", () => get.all(), - ) as containerStatistics[]; + ) as container_stats[]; } diff --git a/src/core/docker/client.ts b/src/core/docker/client.ts index 35876a6e..788a910c 100644 --- a/src/core/docker/client.ts +++ b/src/core/docker/client.ts @@ -1,6 +1,6 @@ import Docker from "dockerode"; import { logger } from "~/core/utils/logger"; -import type { DockerHost } from "../../../typings/docker"; +import type { DockerHost } from "~/typings/docker"; export const getDockerClient = (host: DockerHost): Docker => { try { diff --git a/src/core/docker/monitor.ts b/src/core/docker/monitor.ts index d10c3c65..e4a2510c 100644 --- a/src/core/docker/monitor.ts +++ b/src/core/docker/monitor.ts @@ -68,7 +68,7 @@ async function startFor(host: DockerHost) { if (event.Type === "container") { const containerInfo: ContainerInfo = { id: event.Actor?.ID || event.id || "", - hostId: host.name, + hostId: host.id, name: event.Actor?.Attributes?.name || "", image: event.Actor?.Attributes?.image || event.from || "", status: event.status || event.Actor?.Attributes?.status || "", diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index 8754fd66..63f5ef15 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -2,7 +2,7 @@ import { dbFunctions } from "~/core/database"; import storeContainerData from "~/core/docker/store-container-stats"; import storeHostData from "~/core/docker/store-host-stats"; import { logger } from "~/core/utils/logger"; -import type { config } from "../../../typings/database"; +import type { config } from "~/typings/database"; function convertFromMinToMs(minutes: number): number { return minutes * 60 * 1000; diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts index 33b9c0fb..a2778777 100644 --- a/src/core/docker/store-container-stats.ts +++ b/src/core/docker/store-container-stats.ts @@ -5,6 +5,7 @@ import { calculateCpuPercent, calculateMemoryUsage, } from "~/core/utils/calculations"; +import type { container_stats } from "~/typings/database"; import { logger } from "../utils/logger"; async function storeContainerData() { @@ -68,16 +69,18 @@ async function storeContainerData() { }, ); - dbFunctions.addContainerStats( - containerInfo.Id, - host.name, - containerName, - containerInfo.Image, - containerInfo.Status, - containerInfo.State, - calculateCpuPercent(stats), - calculateMemoryUsage(stats), - ); + const parsed: container_stats = { + cpu_usage: calculateCpuPercent(stats), + hostId: host.id, + id: containerInfo.Id, + image: containerInfo.Image, + memory_usage: calculateMemoryUsage(stats), + name: containerName, + state: containerInfo.State, + status: containerInfo.Status, + }; + + dbFunctions.addContainerStats(parsed); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts index 3c058e7c..2cfb45a4 100644 --- a/src/core/plugins/loader.ts +++ b/src/core/plugins/loader.ts @@ -43,7 +43,7 @@ export async function loadPlugins(pluginDir: string) { pluginManager.register(plugin); pluginCount++; } catch (error) { - pluginManager.fail({ name: file }); + pluginManager.fail({ name: file, version: "0.0.0" }); logger.error( `Error while registering plugin ${absolutePath}: ${error as string}`, ); diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index e0bf121e..c25ea4be 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "node:events"; -import type { ContainerInfo } from "../../../typings/docker"; -import type { Plugin, PluginInfo } from "../../../typings/plugin"; +import type { ContainerInfo } from "~/typings/docker"; +import type { Plugin, PluginInfo } from "~/typings/plugin"; import { logger } from "../utils/logger"; function getHooks(plugin: Plugin) { @@ -52,29 +52,31 @@ class PluginManager extends EventEmitter { } getPlugins(): PluginInfo[] { - const loadedPlugins = Array.from(this.plugins.values()).map((plugin) => { + const plugins: PluginInfo[] = []; + + for (const plugin of this.plugins.values()) { logger.debug(`Loaded plugin: ${plugin}`); const hooks = getHooks(plugin); - return { + plugins.push({ name: plugin.name, + version: plugin.version, status: "active", usedHooks: hooks, - }; - }); - - const failedPlugins = Array.from(this.failedPlugins.values()).map( - (plugin) => { - const hooks = getHooks(plugin); - - return { - name: plugin.name, - status: "inactive", - usedHooks: hooks, - }; - }, - ); - - return loadedPlugins.concat(failedPlugins); + }); + } + + for (const plugin of this.failedPlugins.values()) { + logger.debug(`Loaded plugin: ${plugin}`); + const hooks = getHooks(plugin); + plugins.push({ + name: plugin.name, + version: plugin.version, + status: "inactive", + usedHooks: hooks, + }); + } + + return plugins; } // Trigger plugin flows: diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 193c6a26..399c946d 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -35,6 +35,7 @@ export async function deployStack(stack_config: stacks_config): Promise { postToClient({ type: "stack-status", + timestamp: new Date(), data: { stack_id: stackId, status: "pending", @@ -66,6 +67,7 @@ export async function deployStack(stack_config: stacks_config): Promise { postToClient({ type: "stack-status", + timestamp: new Date(), data: { stack_id: stackId, status: "deployed", @@ -109,6 +111,7 @@ export async function deployStack(stack_config: stacks_config): Promise { postToClient({ type: "stack-error", + timestamp: new Date(), data: { stack_id: stackId ?? 0, action: "deploying", @@ -210,6 +213,7 @@ export async function removeStack(stack_id: number): Promise { logger.error(errorMsg); postToClient({ type: "stack-error", + timestamp: new Date(), data: { stack_id, action: "removing", @@ -224,6 +228,7 @@ export async function removeStack(stack_id: number): Promise { postToClient({ type: "stack-removed", + timestamp: new Date(), data: { stack_id, message: "Stack removed successfully", @@ -234,6 +239,7 @@ export async function removeStack(stack_id: number): Promise { logger.error(errorMsg); postToClient({ type: "stack-error", + timestamp: new Date(), data: { stack_id, action: "removing", diff --git a/src/core/stacks/operations/runStackCommand.ts b/src/core/stacks/operations/runStackCommand.ts index f4fdf739..34ac112a 100644 --- a/src/core/stacks/operations/runStackCommand.ts +++ b/src/core/stacks/operations/runStackCommand.ts @@ -1,6 +1,6 @@ -import type { Stack } from "~/../typings/docker-compose"; import { logger } from "~/core/utils/logger"; import { postToClient } from "~/handlers/modules/live-stacks"; +import type { Stack } from "~/typings/docker-compose"; import { getStackName, getStackPath } from "./stackHelpers"; export function wrapProgressCallback(progressCallback?: (log: string) => void) { @@ -51,6 +51,7 @@ export async function runStackCommand( postToClient({ type: "stack-progress", + timestamp: new Date(), data: { stack_id, action, @@ -77,6 +78,7 @@ export async function runStackCommand( ); postToClient({ type: "stack-error", + timestamp: new Date(), data: { stack_id, action, diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index b9f3863a..bcae15f1 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import chalk, { type ChalkInstance } from "chalk"; +import chalk, { type ChalkFunction } from "chalk"; import type { TransformableInfo } from "logform"; import { createLogger, format, transports } from "winston"; import wrapAnsi from "wrap-ansi"; @@ -8,7 +8,7 @@ import { dbFunctions } from "~/core/database"; import { logToClients } from "~/handlers/modules/logs-socket"; -import type { log_message } from "../../../typings/database"; +import type { log_message } from "~/typings/database"; import { backupInProgress } from "../database/_dbState"; @@ -53,7 +53,7 @@ const formatTerminalMessage = (message: string, prefix: string): string => { } }; -const levelColors: Record = { +const levelColors: Record = { error: chalk.red.bold, warn: chalk.yellow.bold, info: chalk.green.bold, diff --git a/src/core/utils/package-json.ts b/src/core/utils/package-json.ts index 20958a4c..86f9287f 100644 --- a/src/core/utils/package-json.ts +++ b/src/core/utils/package-json.ts @@ -1,4 +1,4 @@ -import packageJson from "~/../package.json"; +import packageJson from "../../../package.json"; const { version, description, license, dependencies, devDependencies } = packageJson; diff --git a/src/handlers/config.ts b/src/handlers/config.ts index 96ecadbd..8ab1f3f2 100644 --- a/src/handlers/config.ts +++ b/src/handlers/config.ts @@ -14,9 +14,9 @@ import { license, version, } from "~/core/utils/package-json"; -import type { config } from "../../typings/database"; -import type { DockerHost } from "../../typings/docker"; -import type { PluginInfo } from "../../typings/plugin"; +import type { config } from "~/typings/database"; +import type { DockerHost } from "~/typings/docker"; +import type { PluginInfo } from "~/typings/plugin"; class apiHandler { getConfig() { diff --git a/src/handlers/docker.ts b/src/handlers/docker.ts index 02e10dc6..47df0612 100644 --- a/src/handlers/docker.ts +++ b/src/handlers/docker.ts @@ -3,12 +3,8 @@ import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; -import type { - ContainerInfo, - DockerHost, - HostStats, -} from "../../typings/docker"; -import type { DockerInfo } from "../../typings/dockerode"; +import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; +import type { DockerInfo } from "~/typings/dockerode"; class basicDockerHandler { async getContainers(): Promise { diff --git a/src/handlers/modules/docker-socket.ts b/src/handlers/modules/docker-socket.ts index 472f9c5c..86b69d76 100644 --- a/src/handlers/modules/docker-socket.ts +++ b/src/handlers/modules/docker-socket.ts @@ -7,7 +7,7 @@ import { calculateMemoryUsage, } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; -import type { DockerStatsEvent } from "../../../typings/docker"; +import type { DockerStatsEvent } from "~/typings/docker"; export function createDockerStatsStream(): Readable { const stream = new Readable({ diff --git a/src/plugins/example.plugin.ts b/src/plugins/example.plugin.ts index 38110e06..178ea705 100644 --- a/src/plugins/example.plugin.ts +++ b/src/plugins/example.plugin.ts @@ -1,7 +1,7 @@ import { logger } from "~/core/utils/logger"; -import type { ContainerInfo } from "../../typings/docker"; -import type { Plugin } from "../../typings/plugin"; +import type { ContainerInfo } from "~/typings/docker"; +import type { Plugin } from "~/typings/plugin"; // See https://outline.itsnik.de/s/dockstat/doc/plugin-development-3UBj9gNMKF for more info diff --git a/src/typings b/src/typings deleted file mode 160000 index d0d22fa6..00000000 --- a/src/typings +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d0d22fa622c5dd9d298d358d4215c8b54cb5f4f3 diff --git a/tsconfig.json b/tsconfig.json index 85c0ed8c..9847d57d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,10 +29,11 @@ /* Modules */ "module": "ES2022" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + "moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ "paths": { - "~/*": ["./src/*"] + "~/*": ["./src/*"], + "~/typings/*": ["./typings/*"] } /* Specify a set of entries that re-map imports to additional lookup locations. */, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ diff --git a/typings b/typings index e029d99d..242f1eef 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit e029d99dcfbadf80156532ee57dd5f969c696332 +Subproject commit 242f1eeff17da8e7bd6348528856dd8656224854 From 4ec34839b47c9103b214c3ac3f03842411dd5272 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Mon, 30 Jun 2025 19:35:43 +0000 Subject: [PATCH 347/369] Update dependency graphs --- dependency-graph.mmd | 411 +++++---- dependency-graph.svg | 1897 +++++++++++++++++++----------------------- 2 files changed, 1038 insertions(+), 1270 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index db1c046d..aaacb834 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -8,247 +8,216 @@ flowchart LR subgraph 0["src"] 1["index.ts"] -subgraph 6["core"] -subgraph 7["stacks"] -8["checker.ts"] -1R["controller.ts"] -subgraph 1T["operations"] -1U["runStackCommand.ts"] -1V["stackHelpers.ts"] -1W["stackStatus.ts"] +subgraph 2["handlers"] +3["index.ts"] +4["config.ts"] +subgraph P["modules"] +Q["logs-socket.ts"] +1B["docker-socket.ts"] +1D["live-stacks.ts"] end +14["database.ts"] +15["docker.ts"] +19["logs.ts"] +1A["sockets.ts"] +1F["stacks.ts"] +1O["utils.ts"] end -subgraph 9["database"] -A["index.ts"] -B["backup.ts"] -E["_dbState.ts"] -F["database.ts"] -K["helper.ts"] -P["config.ts"] -Q["containerStats.ts"] -R["dockerHosts.ts"] -S["hostStats.ts"] -U["logs.ts"] -V["stacks.ts"] +subgraph B["core"] +subgraph C["database"] +D["index.ts"] +E["backup.ts"] +G["_dbState.ts"] +H["database.ts"] +M["helper.ts"] +S["config.ts"] +T["containerStats.ts"] +U["dockerHosts.ts"] +V["hostStats.ts"] +W["logs.ts"] +X["stacks.ts"] end -subgraph L["utils"] -M["logger.ts"] -W["helpers.ts"] -18["calculations.ts"] -1C["change-me-checker.ts"] -1D["package-json.ts"] -1F["swagger-readme.ts"] -1K["response-handler.ts"] +subgraph N["utils"] +O["logger.ts"] +Y["helpers.ts"] +12["package-json.ts"] +1C["calculations.ts"] end -subgraph Z["docker"] -10["monitor.ts"] -15["client.ts"] -16["scheduler.ts"] -17["store-container-stats.ts"] -19["store-host-stats.ts"] +subgraph Z["plugins"] +10["plugin-manager.ts"] end -subgraph 11["plugins"] -12["plugin-manager.ts"] -1B["loader.ts"] +subgraph 17["docker"] +18["client.ts"] +1P["scheduler.ts"] +1Q["store-container-stats.ts"] +1R["store-host-stats.ts"] end +subgraph 1G["stacks"] +1H["controller.ts"] +1J["checker.ts"] +subgraph 1K["operations"] +1L["runStackCommand.ts"] +1M["stackHelpers.ts"] +1N["stackStatus.ts"] end -subgraph N["routes"] -O["live-logs.ts"] -X["live-stacks.ts"] -1J["api-config.ts"] -1L["docker-manager.ts"] -1M["docker-stats.ts"] -1N["docker-websocket.ts"] -1P["logs.ts"] -1Q["stacks.ts"] end -subgraph 1G["middleware"] -1H["auth.ts"] end end -subgraph 2["~"] -subgraph 3["typings"] -4["database"] -C["misc"] -T["docker"] -Y["websocket"] -13["plugin"] -1A["dockerode"] -1I["elysiajs"] -1S["docker-compose"] +subgraph 5["~"] +subgraph 6["typings"] +7["database"] +8["docker"] +9["plugin"] +F["misc"] +16["dockerode"] +1E["websocket"] +1I["docker-compose"] end end -5["elysia-remote-dts"] -subgraph D["fs"] -H["promises"] +subgraph A["fs"] +J["promises"] end -G["bun:sqlite"] -I["os"] -J["path"] -14["events"] -1E["package.json"] -1O["stream"] -1-->8 -1-->X -1-->A -1-->10 -1-->16 -1-->1B -1-->M -1-->1D -1-->1F -1-->1H -1-->1J -1-->1L -1-->1M -1-->1N -1-->O -1-->1P -1-->1Q -1-->4 -1-->5 -8-->A -8-->M -A-->B -A-->P -A-->Q -A-->F -A-->R -A-->S -A-->U -A-->V -B-->E -B-->F -B-->K -B-->M -B-->C -B-->D -F-->G -F-->D -F-->H -F-->I -F-->J -K-->E -K-->M -M-->E -M-->A +I["bun:sqlite"] +K["os"] +L["path"] +R["stream"] +11["events"] +13["package.json"] +1-->3 +3-->4 +3-->14 +3-->15 +3-->19 +3-->1A +3-->1F +3-->1O +3-->1P +4-->D +4-->E +4-->10 +4-->O +4-->12 +4-->7 +4-->8 +4-->9 +4-->A +D-->E +D-->S +D-->T +D-->H +D-->U +D-->V +D-->W +D-->X +E-->G +E-->H +E-->M +E-->O +E-->F +E-->A +H-->I +H-->A +H-->J +H-->K +H-->L +M-->G M-->O -M-->4 -M-->J -O-->M -O-->4 -P-->F -P-->K -Q-->F -Q-->K -R-->F -R-->K -S-->F -S-->K -S-->T -U-->F -U-->K -U-->4 -V-->W -V-->F -V-->K -V-->4 +O-->G +O-->D +O-->Q +O-->7 +O-->L +Q-->O +Q-->7 +Q-->R +S-->H +S-->M +T-->H +T-->M +T-->7 +U-->H +U-->M +U-->8 +V-->H +V-->M +V-->8 +W-->H W-->M -X-->M +W-->7 X-->Y -10-->12 -10-->A -10-->15 -10-->M -10-->T -12-->M -12-->T +X-->H +X-->M +X-->7 +Y-->O +10-->O +10-->8 +10-->9 +10-->11 12-->13 -12-->14 -15-->M -15-->T -16-->A -16-->17 -16-->19 -16-->M -16-->4 -17-->M -17-->A -17-->15 -17-->18 -19-->A -19-->15 -19-->W -19-->M -19-->T -19-->1A -1B-->1C -1B-->M -1B-->12 +14-->D +15-->D +15-->18 +15-->Y +15-->O +15-->8 +15-->16 +18-->O +18-->8 +19-->D +19-->O +1A-->1B +1A-->1D +1A-->Q 1B-->D -1B-->J -1C-->M -1C-->H +1B-->18 +1B-->1C +1B-->O +1B-->8 +1B-->R +1D-->O 1D-->1E -1H-->A -1H-->M -1H-->4 +1D-->R +1F-->D +1F-->1H +1F-->O +1F-->7 +1H-->1J +1H-->1L +1H-->1M +1H-->1N +1H-->D +1H-->O +1H-->1D +1H-->7 1H-->1I -1J-->A -1J-->B -1J-->12 -1J-->M -1J-->1D -1J-->1K -1J-->1H -1J-->4 +1H-->J 1J-->D -1K-->M -1K-->1I -1L-->A -1L-->M -1L-->1K -1L-->T -1M-->A -1M-->15 -1M-->18 -1M-->W -1M-->M -1M-->1K -1M-->T -1M-->1A -1N-->A -1N-->15 -1N-->18 -1N-->M -1N-->1K -1N-->1O -1P-->A -1P-->M -1Q-->A -1Q-->1R -1Q-->M -1Q-->1K -1Q-->4 +1J-->O +1L-->1M +1L-->O +1L-->1D +1L-->1I +1M-->D +1M-->Y +1M-->O +1M-->1I +1N-->1L +1N-->D +1N-->O +1O-->O +1P-->D +1P-->1Q +1P-->1R +1P-->O +1P-->7 +1Q-->O +1Q-->D +1Q-->18 +1Q-->1C +1Q-->7 +1R-->D +1R-->18 +1R-->O 1R-->8 -1R-->1U -1R-->1V -1R-->1W -1R-->A -1R-->M -1R-->X -1R-->4 -1R-->1S -1R-->H -1U-->1V -1U-->M -1U-->X -1U-->1S -1V-->A -1V-->W -1V-->M -1V-->1S -1W-->1U -1W-->A -1W-->M +1R-->16 diff --git a/dependency-graph.svg b/dependency-graph.svg index 54234f89..024d4c75 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,1608 +4,1407 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/stacks/operations - -operations + +operations cluster_src/core/utils - -utils + +utils -cluster_src/middleware - -middleware +cluster_src/handlers + +handlers -cluster_src/routes - -routes +cluster_src/handlers/modules + +modules cluster_~ - -~ + +~ cluster_~/typings - -typings + +typings bun:sqlite - -bun:sqlite - - - - - -elysia-remote-dts - - -elysia-remote-dts + +bun:sqlite - + events - - -events + + +events - + fs - - -fs + + +fs - + fs/promises - - -promises + + +promises - + os - - -os + + +os - + package.json - - -package.json + + +package.json - + path - - -path + + +path - + src/core/database/_dbState.ts - - -_dbState.ts + + +_dbState.ts - + src/core/database/backup.ts - - -backup.ts + + +backup.ts src/core/database/backup.ts->fs - - + + src/core/database/backup.ts->src/core/database/_dbState.ts - - + + - + src/core/database/database.ts - - -database.ts + + +database.ts src/core/database/backup.ts->src/core/database/database.ts - - + + - + src/core/database/helper.ts - - -helper.ts + + +helper.ts src/core/database/backup.ts->src/core/database/helper.ts - - - - + + + + - + src/core/utils/logger.ts - - -logger.ts + + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + - + ~/typings/misc - - -misc + + +misc src/core/database/backup.ts->~/typings/misc - - + + - + src/core/database/database.ts->bun:sqlite - - + + - + src/core/database/database.ts->fs - - + + - + src/core/database/database.ts->fs/promises - - + + - + src/core/database/database.ts->os - - + + - + src/core/database/database.ts->path - - + + - + src/core/database/helper.ts->src/core/database/_dbState.ts - - + + - + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + - + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + + + + +~/typings/database + + +database + + + + + +src/core/utils/logger.ts->~/typings/database + + src/core/database/index.ts - -index.ts + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + - - -~/typings/database - - -database - - - - - -src/core/utils/logger.ts->~/typings/database - - - - - -src/routes/live-logs.ts - - -live-logs.ts + + +src/handlers/modules/logs-socket.ts + + +logs-socket.ts - - -src/core/utils/logger.ts->src/routes/live-logs.ts - - - - + + +src/core/utils/logger.ts->src/handlers/modules/logs-socket.ts + + + + - + src/core/database/config.ts - - -config.ts + + +config.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/containerStats.ts - - -containerStats.ts + + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + + + + +src/core/database/containerStats.ts->~/typings/database + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts - + src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + - + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + - + +~/typings/docker + + +docker + + + + + +src/core/database/dockerHosts.ts->~/typings/docker + + + + + src/core/database/hostStats.ts - - -hostStats.ts + + +hostStats.ts - + src/core/database/hostStats.ts->src/core/database/database.ts - - + + - + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - - - - -~/typings/docker - - -docker - - + + + + - + src/core/database/hostStats.ts->~/typings/docker - - + + - + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/database.ts - - + + - + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + - + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts - + src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts - + src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + - + src/core/database/logs.ts->src/core/database/database.ts - - + + - + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/logs.ts->~/typings/database - - + + - + src/core/database/stacks.ts->src/core/database/database.ts - - + + - + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/stacks.ts->~/typings/database - - + + - + src/core/utils/helpers.ts - - -helpers.ts + + +helpers.ts - + src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - + + + + - + src/core/utils/helpers.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/docker/client.ts - - -client.ts + + +client.ts - + src/core/docker/client.ts->src/core/utils/logger.ts - - + + - -src/core/docker/client.ts->~/typings/docker - - - - - -src/core/docker/monitor.ts - - -monitor.ts - - - - - -src/core/docker/monitor.ts->src/core/utils/logger.ts - - - - - -src/core/docker/monitor.ts->~/typings/docker - - - - -src/core/docker/monitor.ts->src/core/database/index.ts - - - - - -src/core/docker/monitor.ts->src/core/docker/client.ts - - - - - -src/core/plugins/plugin-manager.ts - - -plugin-manager.ts - - - - - -src/core/docker/monitor.ts->src/core/plugins/plugin-manager.ts - - - - - -src/core/plugins/plugin-manager.ts->events - - - - - -src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - - - - -src/core/plugins/plugin-manager.ts->~/typings/docker - - - - - -~/typings/plugin - - -plugin - - - - - -src/core/plugins/plugin-manager.ts->~/typings/plugin - - +src/core/docker/client.ts->~/typings/docker + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - -src/core/docker/scheduler.ts->src/core/utils/logger.ts - - - - -src/core/docker/scheduler.ts->src/core/database/index.ts - - +src/core/docker/scheduler.ts->src/core/utils/logger.ts + + - + src/core/docker/scheduler.ts->~/typings/database - - + + + + + +src/core/docker/scheduler.ts->src/core/database/index.ts + + - + src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + - + +src/core/docker/store-container-stats.ts->~/typings/database + + + + + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - - -calculations.ts + + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-host-stats.ts->~/typings/docker - - + + - + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - - - - -src/core/docker/store-host-stats.ts->src/core/utils/helpers.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + ~/typings/dockerode - - -dockerode + + +dockerode - + src/core/docker/store-host-stats.ts->~/typings/dockerode - - + + - - -src/core/plugins/loader.ts - - -loader.ts + + +src/core/plugins/plugin-manager.ts + + +plugin-manager.ts - - -src/core/plugins/loader.ts->fs - - - - - -src/core/plugins/loader.ts->path - - + + +src/core/plugins/plugin-manager.ts->events + + - - -src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + +src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts + + - - -src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - + + +src/core/plugins/plugin-manager.ts->~/typings/docker + + - - -src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +~/typings/plugin + + +plugin - - -src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - - - - -src/core/utils/change-me-checker.ts->fs/promises - - - - - -src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + +src/core/plugins/plugin-manager.ts->~/typings/plugin + + - + src/core/stacks/checker.ts - - -checker.ts + + +checker.ts - + src/core/stacks/checker.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/checker.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - - - - -src/core/stacks/controller.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts->~/typings/database - - + + + + + +src/core/stacks/controller.ts->src/core/database/index.ts + + - + src/core/stacks/controller.ts->src/core/stacks/checker.ts - - + + - + src/core/stacks/operations/runStackCommand.ts - - -runStackCommand.ts + + +runStackCommand.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/runStackCommand.ts - - + + - + src/core/stacks/operations/stackHelpers.ts - - -stackHelpers.ts + + +stackHelpers.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/stackHelpers.ts - - + + - + src/core/stacks/operations/stackStatus.ts - - -stackStatus.ts + + +stackStatus.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/stackStatus.ts - - + + - - -src/routes/live-stacks.ts - - -live-stacks.ts + + +src/handlers/modules/live-stacks.ts + + +live-stacks.ts - - -src/core/stacks/controller.ts->src/routes/live-stacks.ts - - + + +src/core/stacks/controller.ts->src/handlers/modules/live-stacks.ts + + - + ~/typings/docker-compose - - -docker-compose + + +docker-compose - + src/core/stacks/controller.ts->~/typings/docker-compose - - + + - + src/core/stacks/operations/runStackCommand.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/operations/runStackCommand.ts->src/core/stacks/operations/stackHelpers.ts - - + + - - -src/core/stacks/operations/runStackCommand.ts->src/routes/live-stacks.ts - - + + +src/core/stacks/operations/runStackCommand.ts->src/handlers/modules/live-stacks.ts + + - + src/core/stacks/operations/runStackCommand.ts->~/typings/docker-compose - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/database/index.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/utils/helpers.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->~/typings/docker-compose - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/database/index.ts - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/stacks/operations/runStackCommand.ts - - + + + + + +src/handlers/modules/live-stacks.ts->src/core/utils/logger.ts + + + + + +stream + + +stream + + - - -src/routes/live-stacks.ts->src/core/utils/logger.ts - - + + +src/handlers/modules/live-stacks.ts->stream + + - + ~/typings/websocket - - -websocket + + +websocket - - -src/routes/live-stacks.ts->~/typings/websocket - - + + +src/handlers/modules/live-stacks.ts->~/typings/websocket + + - - -src/routes/live-logs.ts->src/core/utils/logger.ts - - - - + + +src/handlers/modules/logs-socket.ts->src/core/utils/logger.ts + + + + - - -src/routes/live-logs.ts->~/typings/database - - + + +src/handlers/modules/logs-socket.ts->~/typings/database + + + + + +src/handlers/modules/logs-socket.ts->stream + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - - -src/core/utils/response-handler.ts - - -response-handler.ts + + +src/handlers/config.ts + + +config.ts - - -src/core/utils/response-handler.ts->src/core/utils/logger.ts - - + + +src/handlers/config.ts->fs + + - - -~/typings/elysiajs - - -elysiajs - + + +src/handlers/config.ts->src/core/database/backup.ts + + + + +src/handlers/config.ts->src/core/utils/logger.ts + + - - -src/core/utils/response-handler.ts->~/typings/elysiajs - - + + +src/handlers/config.ts->~/typings/database + + - - -src/core/utils/swagger-readme.ts - - -swagger-readme.ts - + + +src/handlers/config.ts->~/typings/docker + + + + +src/handlers/config.ts->src/core/database/index.ts + + - - -src/index.ts - - -index.ts - + + +src/handlers/config.ts->src/core/plugins/plugin-manager.ts + + + + +src/handlers/config.ts->~/typings/plugin + + - - -src/index.ts->elysia-remote-dts - - + + +src/handlers/config.ts->src/core/utils/package-json.ts + + - - -src/index.ts->src/core/utils/logger.ts - - + + +src/handlers/database.ts + + +database.ts + - - -src/index.ts->src/core/database/index.ts - - - - -src/index.ts->~/typings/database - - + + +src/handlers/database.ts->src/core/database/index.ts + + - - -src/index.ts->src/core/docker/monitor.ts - - + + +src/handlers/docker.ts + + +docker.ts + - - -src/index.ts->src/core/docker/scheduler.ts - - - - -src/index.ts->src/core/plugins/loader.ts - - + + +src/handlers/docker.ts->src/core/utils/logger.ts + + - - -src/index.ts->src/core/stacks/checker.ts - - + + +src/handlers/docker.ts->~/typings/docker + + - - -src/index.ts->src/routes/live-stacks.ts - - + + +src/handlers/docker.ts->src/core/database/index.ts + + - - -src/index.ts->src/routes/live-logs.ts - - + + +src/handlers/docker.ts->src/core/utils/helpers.ts + + - - -src/index.ts->src/core/utils/package-json.ts - - + + +src/handlers/docker.ts->src/core/docker/client.ts + + - - -src/index.ts->src/core/utils/swagger-readme.ts - - + + +src/handlers/docker.ts->~/typings/dockerode + + - - -src/middleware/auth.ts - - -auth.ts + + +src/handlers/index.ts + + +index.ts - - -src/index.ts->src/middleware/auth.ts - - + + +src/handlers/index.ts->src/core/docker/scheduler.ts + + - - -src/routes/api-config.ts - - -api-config.ts - + + +src/handlers/index.ts->src/handlers/config.ts + + + + +src/handlers/index.ts->src/handlers/database.ts + + - - -src/index.ts->src/routes/api-config.ts - - + + +src/handlers/index.ts->src/handlers/docker.ts + + - - -src/routes/docker-manager.ts - - -docker-manager.ts + + +src/handlers/logs.ts + + +logs.ts - - -src/index.ts->src/routes/docker-manager.ts - - + + +src/handlers/index.ts->src/handlers/logs.ts + + - - -src/routes/docker-stats.ts - - -docker-stats.ts + + +src/handlers/sockets.ts + + +sockets.ts - - -src/index.ts->src/routes/docker-stats.ts - - - - - -src/routes/docker-websocket.ts - - -docker-websocket.ts - - + + +src/handlers/index.ts->src/handlers/sockets.ts + + - - -src/index.ts->src/routes/docker-websocket.ts - - - - - -src/routes/logs.ts - - -logs.ts + + +src/handlers/stacks.ts + + +stacks.ts - - -src/index.ts->src/routes/logs.ts - - - - - -src/routes/stacks.ts - - -stacks.ts + + +src/handlers/index.ts->src/handlers/stacks.ts + + + + + +src/handlers/utils.ts + + +utils.ts - - -src/index.ts->src/routes/stacks.ts - - + + +src/handlers/index.ts->src/handlers/utils.ts + + - - -src/middleware/auth.ts->src/core/utils/logger.ts - - + + +src/handlers/logs.ts->src/core/utils/logger.ts + + - - -src/middleware/auth.ts->src/core/database/index.ts - - + + +src/handlers/logs.ts->src/core/database/index.ts + + - - -src/middleware/auth.ts->~/typings/database - - + + +src/handlers/sockets.ts->src/handlers/modules/live-stacks.ts + + - - -src/middleware/auth.ts->~/typings/elysiajs - - + + +src/handlers/sockets.ts->src/handlers/modules/logs-socket.ts + + - - -src/routes/api-config.ts->fs - - + + +src/handlers/modules/docker-socket.ts + + +docker-socket.ts + - - -src/routes/api-config.ts->src/core/database/backup.ts - - - + -src/routes/api-config.ts->src/core/utils/logger.ts - - +src/handlers/sockets.ts->src/handlers/modules/docker-socket.ts + + - - -src/routes/api-config.ts->src/core/database/index.ts - - + + +src/handlers/stacks.ts->src/core/utils/logger.ts + + - - -src/routes/api-config.ts->~/typings/database - - + + +src/handlers/stacks.ts->~/typings/database + + - - -src/routes/api-config.ts->src/core/plugins/plugin-manager.ts - - + + +src/handlers/stacks.ts->src/core/database/index.ts + + - - -src/routes/api-config.ts->src/core/utils/package-json.ts - - + + +src/handlers/stacks.ts->src/core/stacks/controller.ts + + - - -src/routes/api-config.ts->src/core/utils/response-handler.ts - - + + +src/handlers/utils.ts->src/core/utils/logger.ts + + - - -src/routes/api-config.ts->src/middleware/auth.ts - - + + +src/handlers/modules/docker-socket.ts->src/core/utils/logger.ts + + - - -src/routes/docker-manager.ts->src/core/utils/logger.ts - - + + +src/handlers/modules/docker-socket.ts->~/typings/docker + + - - -src/routes/docker-manager.ts->~/typings/docker - - + + +src/handlers/modules/docker-socket.ts->src/core/database/index.ts + + - - -src/routes/docker-manager.ts->src/core/database/index.ts - - + + +src/handlers/modules/docker-socket.ts->src/core/docker/client.ts + + - - -src/routes/docker-manager.ts->src/core/utils/response-handler.ts - - - - - -src/routes/docker-stats.ts->src/core/utils/logger.ts - - - - - -src/routes/docker-stats.ts->~/typings/docker - - - - - -src/routes/docker-stats.ts->src/core/database/index.ts - - - - - -src/routes/docker-stats.ts->src/core/utils/helpers.ts - - - - - -src/routes/docker-stats.ts->src/core/docker/client.ts - - - - - -src/routes/docker-stats.ts->src/core/utils/calculations.ts - - - - - -src/routes/docker-stats.ts->~/typings/dockerode - - - - - -src/routes/docker-stats.ts->src/core/utils/response-handler.ts - - - - - -src/routes/docker-websocket.ts->src/core/utils/logger.ts - - - - - -src/routes/docker-websocket.ts->src/core/database/index.ts - - - - - -src/routes/docker-websocket.ts->src/core/docker/client.ts - - - - - -src/routes/docker-websocket.ts->src/core/utils/calculations.ts - - - - - -src/routes/docker-websocket.ts->src/core/utils/response-handler.ts - - + + +src/handlers/modules/docker-socket.ts->src/core/utils/calculations.ts + + - - -stream - - -stream + + +src/handlers/modules/docker-socket.ts->stream + + + + + +src/index.ts + + +index.ts - - -src/routes/docker-websocket.ts->stream - - - - - -src/routes/logs.ts->src/core/utils/logger.ts - - - - - -src/routes/logs.ts->src/core/database/index.ts - - - - - -src/routes/stacks.ts->src/core/utils/logger.ts - - - - - -src/routes/stacks.ts->src/core/database/index.ts - - - - - -src/routes/stacks.ts->~/typings/database - - - - - -src/routes/stacks.ts->src/core/stacks/controller.ts - - - - - -src/routes/stacks.ts->src/core/utils/response-handler.ts - - + + +src/index.ts->src/handlers/index.ts + + From 8ba88236c2cea08360668f68f1974b277694bb6c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Wed, 2 Jul 2025 07:58:09 +0200 Subject: [PATCH 348/369] save point; plugin manager adjustments --- src/core/plugins/loader.ts | 2 +- src/core/plugins/plugin-manager.ts | 10 +++ src/handlers/docker.ts | 129 ++++++++++++++--------------- src/handlers/index.ts | 4 +- typings | 2 +- 5 files changed, 79 insertions(+), 68 deletions(-) diff --git a/src/core/plugins/loader.ts b/src/core/plugins/loader.ts index 2cfb45a4..c6da8764 100644 --- a/src/core/plugins/loader.ts +++ b/src/core/plugins/loader.ts @@ -38,7 +38,7 @@ export async function loadPlugins(pluginDir: string) { logger.info(`Loading plugin: ${absolutePath}`); try { await checkFileForChangeMe(absolutePath); - const module = await import(absolutePath); + const module = await import(/* @vite-ignore */ absolutePath); const plugin = module.default; pluginManager.register(plugin); pluginCount++; diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index c25ea4be..ad19169e 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -2,6 +2,7 @@ import { EventEmitter } from "node:events"; import type { ContainerInfo } from "~/typings/docker"; import type { Plugin, PluginInfo } from "~/typings/plugin"; import { logger } from "../utils/logger"; +import { loadPlugins } from "./loader"; function getHooks(plugin: Plugin) { return { @@ -27,6 +28,15 @@ class PluginManager extends EventEmitter { private plugins: Map = new Map(); private failedPlugins: Map = new Map(); + async start() { + try { + return await loadPlugins("./server/src/plugins"); + } catch (error) { + logger.error(`Failed to init plugin manager: ${error}`); + return; + } + } + fail(plugin: Plugin) { try { this.failedPlugins.set(plugin.name, plugin); diff --git a/src/handlers/docker.ts b/src/handlers/docker.ts index 47df0612..6e6a5411 100644 --- a/src/handlers/docker.ts +++ b/src/handlers/docker.ts @@ -1,7 +1,6 @@ import type Docker from "dockerode"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; -import { findObjectByKey } from "~/core/utils/helpers"; import { logger } from "~/core/utils/logger"; import type { ContainerInfo, DockerHost, HostStats } from "~/typings/docker"; import type { DockerInfo } from "~/typings/dockerode"; @@ -79,77 +78,77 @@ class basicDockerHandler { } } - async getHostStats(id?: number) { - if (!id) { - try { - const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - - const stats: HostStats[] = []; - - for (const host of hosts) { - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - stats.push(config); - } - - logger.debug("Fetched all hosts"); - return stats; - } catch (error) { - throw new Error(error as string); - } - } - + async getHostStats() { + //if (true) { try { const hosts = dbFunctions.getDockerHosts() as DockerHost[]; - const host = findObjectByKey(hosts, "id", Number(id)); - if (!host) { - throw new Error(`Host (${id}) not found`); + const stats: HostStats[] = []; + + for (const host of hosts) { + const docker = getDockerClient(host); + const info: DockerInfo = await docker.info(); + + const config: HostStats = { + hostId: host.id as number, + hostName: host.name, + dockerVersion: info.ServerVersion, + apiVersion: info.Driver, + os: info.OperatingSystem, + architecture: info.Architecture, + totalMemory: info.MemTotal, + totalCPU: info.NCPU, + labels: info.Labels, + images: info.Images, + containers: info.Containers, + containersPaused: info.ContainersPaused, + containersRunning: info.ContainersRunning, + containersStopped: info.ContainersStopped, + }; + + stats.push(config); } - const docker = getDockerClient(host); - const info: DockerInfo = await docker.info(); - - const config: HostStats = { - hostId: host.id as number, - hostName: host.name, - dockerVersion: info.ServerVersion, - apiVersion: info.Driver, - os: info.OperatingSystem, - architecture: info.Architecture, - totalMemory: info.MemTotal, - totalCPU: info.NCPU, - labels: info.Labels, - images: info.Images, - containers: info.Containers, - containersPaused: info.ContainersPaused, - containersRunning: info.ContainersRunning, - containersStopped: info.ContainersStopped, - }; - - logger.debug(`Fetched config for ${host.name}`); - return config; + logger.debug("Fetched all hosts"); + return stats; } catch (error) { - throw new Error(`Failed to retrieve host config: ${error}`); + throw new Error(error as string); } + //} + + //try { + // const hosts = dbFunctions.getDockerHosts() as DockerHost[]; + // + // const host = findObjectByKey(hosts, "id", Number(id)); + // if (!host) { + // throw new Error(`Host (${id}) not found`); + // } + // + // const docker = getDockerClient(host); + // const info: DockerInfo = await docker.info(); + // + // const config: HostStats = { + // hostId: host.id as number, + // hostName: host.name, + // dockerVersion: info.ServerVersion, + // apiVersion: info.Driver, + // os: info.OperatingSystem, + // architecture: info.Architecture, + // totalMemory: info.MemTotal, + // totalCPU: info.NCPU, + // labels: info.Labels, + // images: info.Images, + // containers: info.Containers, + // containersPaused: info.ContainersPaused, + // containersRunning: info.ContainersRunning, + // containersStopped: info.ContainersStopped, + // }; + // + // logger.debug(`Fetched config for ${host.name}`); + // return config; + //} catch (error) { + // throw new Error(`Failed to retrieve host config: ${error}`); + //} } } diff --git a/src/handlers/index.ts b/src/handlers/index.ts index b0ca70b4..b025dbd9 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -1,4 +1,5 @@ import { setSchedules } from "~/core/docker/scheduler"; +import { pluginManager } from "~/core/plugins/plugin-manager"; import { ApiHandler } from "./config"; import { DatabaseHandler } from "./database"; import { BasicDockerHandler } from "./docker"; @@ -15,5 +16,6 @@ export const handlers = { LogHandler, CheckHealth, Sockets: Sockets, - Start: setSchedules(), + StartServer: setSchedules(), + ImportPlugins: await pluginManager.start(), }; diff --git a/typings b/typings index 242f1eef..2c22e7f3 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 242f1eeff17da8e7bd6348528856dd8656224854 +Subproject commit 2c22e7f3fff362939c79291a097ee5b13d700a79 From 6ef8769c351ab4e29e85836fe05b7f3248b1b555 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 3 Jul 2025 15:21:57 +0200 Subject: [PATCH 349/369] Feat: Websocket server for relaying stats --- data/.gitignore | 1 - package.json | 2 +- src/core/database/backup.ts | 12 +- src/core/docker/scheduler.ts | 2 +- src/core/plugins/plugin-manager.ts | 7 +- src/core/utils/logger.ts | 2 +- src/handlers/config.ts | 1 + src/handlers/index.ts | 7 +- src/handlers/modules/docker-socket.ts | 211 ++++++++++++-------------- src/handlers/modules/starter.ts | 35 +++++ src/handlers/sockets.ts | 6 +- tsconfig.json | 2 +- typings | 2 +- 13 files changed, 158 insertions(+), 132 deletions(-) delete mode 100644 data/.gitignore create mode 100644 src/handlers/modules/starter.ts diff --git a/data/.gitignore b/data/.gitignore deleted file mode 100644 index aed31992..00000000 --- a/data/.gitignore +++ /dev/null @@ -1 +0,0 @@ -./dockstatapi* diff --git a/package.json b/package.json index f2e318b5..94eec8d0 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@types/bun": "latest", "@types/dockerode": "^3.3.42", "@types/js-yaml": "^4.0.9", - "@types/node": "^22.15.32", + "@types/node": "^22.16.0", "@types/split2": "^4.2.3", "bun-types": "latest", "cross-env": "^7.0.3", diff --git a/src/core/database/backup.ts b/src/core/database/backup.ts index 4efa130c..df6a744a 100644 --- a/src/core/database/backup.ts +++ b/src/core/database/backup.ts @@ -60,9 +60,9 @@ export async function backupDatabase(): Promise { copyFileSync(`${backupDir}dockstatapi.db`, backupFilename); logger.info(`Backup created successfully: ${backupFilename}`); logger.debug("File copy operation completed without errors"); - } catch (e) { - logger.error(`Failed to create backup file: ${(e as Error).message}`); - throw e; + } catch (error) { + logger.error(`Failed to create backup file: ${(error as Error).message}`); + throw new Error(error as string); } return backupFilename; @@ -97,9 +97,9 @@ export function restoreDatabase(backupFilename: string): void { copyFileSync(backupFile, `${backupDir}dockstatapi.db`); logger.info(`Database restored successfully from: ${backupFilename}`); logger.debug("Database file replacement completed"); - } catch (e) { - logger.error(`Restore failed: ${(e as Error).message}`); - throw e; + } catch (error) { + logger.error(`Restore failed: ${(error as Error).message}`); + throw new Error(error as string); } }, () => { diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index 63f5ef15..0ac78ad4 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -117,7 +117,7 @@ async function setSchedules() { logger.info("Schedules have been set successfully."); } catch (error) { logger.error("Error setting schedules:", error); - throw error; + throw new Error(error as string); } } diff --git a/src/core/plugins/plugin-manager.ts b/src/core/plugins/plugin-manager.ts index ad19169e..f68b80d8 100644 --- a/src/core/plugins/plugin-manager.ts +++ b/src/core/plugins/plugin-manager.ts @@ -30,7 +30,8 @@ class PluginManager extends EventEmitter { async start() { try { - return await loadPlugins("./server/src/plugins"); + await loadPlugins("./server/src/plugins"); + return; } catch (error) { logger.error(`Failed to init plugin manager: ${error}`); return; @@ -65,7 +66,7 @@ class PluginManager extends EventEmitter { const plugins: PluginInfo[] = []; for (const plugin of this.plugins.values()) { - logger.debug(`Loaded plugin: ${plugin}`); + logger.debug(`Loaded plugin: ${JSON.stringify(plugin)}`); const hooks = getHooks(plugin); plugins.push({ name: plugin.name, @@ -76,7 +77,7 @@ class PluginManager extends EventEmitter { } for (const plugin of this.failedPlugins.values()) { - logger.debug(`Loaded plugin: ${plugin}`); + logger.debug(`Loaded plugin: ${JSON.stringify(plugin)}`); const hooks = getHooks(plugin); plugins.push({ name: plugin.name, diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index bcae15f1..3b9248d2 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -12,7 +12,7 @@ import type { log_message } from "~/typings/database"; import { backupInProgress } from "../database/_dbState"; -const padNewlines = process.env.PAD_NEW_LINES !== "false"; +const padNewlines = true; //process.env.PAD_NEW_LINES !== "false"; type LogLevel = | "error" diff --git a/src/handlers/config.ts b/src/handlers/config.ts index 8ab1f3f2..62491ae3 100644 --- a/src/handlers/config.ts +++ b/src/handlers/config.ts @@ -44,6 +44,7 @@ class apiHandler { getPlugins(): PluginInfo[] { try { + logger.debug("Gathering plugins"); return pluginManager.getPlugins(); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); diff --git a/src/handlers/index.ts b/src/handlers/index.ts index b025dbd9..f5360a2b 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -1,9 +1,12 @@ import { setSchedules } from "~/core/docker/scheduler"; import { pluginManager } from "~/core/plugins/plugin-manager"; +import { logger } from "~/core/utils/logger"; import { ApiHandler } from "./config"; import { DatabaseHandler } from "./database"; import { BasicDockerHandler } from "./docker"; import { LogHandler } from "./logs"; +import { startDockerStatsBroadcast } from "./modules/docker-socket"; +import { Starter } from "./modules/starter"; import { Sockets } from "./sockets"; import { StackHandler } from "./stacks"; import { CheckHealth } from "./utils"; @@ -16,6 +19,6 @@ export const handlers = { LogHandler, CheckHealth, Sockets: Sockets, - StartServer: setSchedules(), - ImportPlugins: await pluginManager.start(), }; + +Starter.startAll(); diff --git a/src/handlers/modules/docker-socket.ts b/src/handlers/modules/docker-socket.ts index 86b69d76..f78eb8d5 100644 --- a/src/handlers/modules/docker-socket.ts +++ b/src/handlers/modules/docker-socket.ts @@ -1,4 +1,4 @@ -import { Readable, type Transform } from "node:stream"; +import { serve } from "bun"; import split2 from "split2"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; @@ -9,134 +9,119 @@ import { import { logger } from "~/core/utils/logger"; import type { DockerStatsEvent } from "~/typings/docker"; -export function createDockerStatsStream(): Readable { - const stream = new Readable({ - objectMode: true, - read() {}, - }); +// Track all connected WebSocket clients +const clients = new Set>(); - const substreams: Array<{ - statsStream: Readable; - splitStream: Transform; - }> = []; - - const cleanup = () => { - for (const { statsStream, splitStream } of substreams) { - try { - statsStream.unpipe(splitStream); - statsStream.destroy(); - splitStream.destroy(); - } catch (error) { - logger.error(`Cleanup error: ${error}`); - } +// Broadcast a DockerStatsEvent to every connected client +function broadcast(event: DockerStatsEvent) { + const message = JSON.stringify(event); + for (const ws of clients) { + if (ws.readyState === 1) { + ws.send(message); } - substreams.length = 0; - }; - - stream.on("close", cleanup); - stream.on("error", cleanup); - - (async () => { - try { - const hosts = dbFunctions.getDockerHosts(); - logger.debug(`Retrieved ${hosts.length} docker host(s)`); - - for (const host of hosts) { - if (stream.destroyed) break; + } +} - try { - const docker = getDockerClient(host); - await docker.ping(); - const containers = await docker.listContainers({ - all: true, - }); +// Start Docker stats polling and broadcasting +export async function startDockerStatsBroadcast() { + logger.debug("Starting Docker stats broadcast..."); - logger.debug( - `Found ${containers.length} containers on ${host.name} (id: ${host.id})`, - ); + try { + const hosts = dbFunctions.getDockerHosts(); + logger.debug(`Retrieved ${hosts.length} Docker host(s)`); - for (const containerInfo of containers) { - if (stream.destroyed) break; + for (const host of hosts) { + try { + const docker = getDockerClient(host); + await docker.ping(); + const containers = await docker.listContainers({ all: true }); + logger.debug( + `Host ${host.name} contains ${containers.length} containers`, + ); + for (const info of containers) { + // Kick off one independent async task per container + (async () => { try { - const container = docker.getContainer(containerInfo.Id); - const statsStream = (await container.stats({ - stream: true, - })) as Readable; - const splitStream = split2(); - - substreams.push({ statsStream, splitStream }); + const statsStream = await docker + .getContainer(info.Id) + .stats({ stream: true }); + const splitter = split2(); + statsStream.pipe(splitter); - statsStream - .on("close", () => splitStream.destroy()) - .pipe(splitStream) - .on("data", (line: string) => { - if (stream.destroyed || !line) return; - - try { - const stats = JSON.parse(line); - const event: DockerStatsEvent = { - type: "stats", - id: containerInfo.Id, - hostId: host.id, - name: containerInfo.Names[0].replace(/^\//, ""), - image: containerInfo.Image, - status: containerInfo.Status, - state: containerInfo.State, - cpuUsage: calculateCpuPercent(stats) ?? 0, - memoryUsage: calculateMemoryUsage(stats) ?? 0, - }; - stream.push(event); - } catch (error) { - stream.push({ - type: "error", - hostId: host.id, - containerId: containerInfo.Id, - error: `Parse error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); - } - }) - .on("error", (error: Error) => { - stream.push({ + for await (const line of splitter) { + if (!line) continue; + try { + const stats = JSON.parse(line); + broadcast({ + type: "stats", + id: info.Id, + hostId: host.id, + name: info.Names[0].replace(/^\//, ""), + image: info.Image, + status: info.Status, + state: stats.state || info.State, + cpuUsage: calculateCpuPercent(stats) ?? 0, + memoryUsage: calculateMemoryUsage(stats) ?? 0, + }); + } catch (err) { + broadcast({ type: "error", hostId: host.id, - containerId: containerInfo.Id, - error: `Stream error: ${error.message}`, + containerId: info.Id, + error: `Parse error: ${(err as Error).message}`, }); - }); - } catch (error) { - stream.push({ + } + } + } catch (err) { + broadcast({ type: "error", hostId: host.id, - containerId: containerInfo.Id, - error: `Container error: ${ - error instanceof Error ? error.message : String(error) - }`, + containerId: info.Id, + error: `Stats stream error: ${(err as Error).message}`, }); } - } - } catch (error) { - stream.push({ - type: "error", - hostId: host.id, - error: `Host connection error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); + })(); } + } catch (err) { + broadcast({ + type: "error", + hostId: host.id, + error: `Host connection error: ${(err as Error).message}`, + }); } - } catch (error) { - stream.push({ - type: "error", - error: `Initialization error: ${ - error instanceof Error ? error.message : String(error) - }`, - }); - stream.destroy(); } - })(); - - return stream; + } catch (err) { + broadcast({ + type: "error", + hostId: 0, + error: `Initialization error: ${(err as Error).message}`, + }); + } } + +serve({ + port: 4837, + reusePort: true, + fetch(req, server) { + // Upgrade requests to WebSocket + if (req.url.endsWith("/ws/docker")) { + if (server.upgrade(req)) { + return; // auto 101 Switching Protocols + } + } + return new Response("Expected WebSocket upgrade", { status: 426 }); + }, + + websocket: { + open(ws) { + logger.debug("Client connected via WebSocket"); + clients.add(ws); + }, + close(ws, code, reason) { + logger.debug(`Client disconnected (${code}): ${reason}`); + clients.delete(ws); + }, + message() {}, + }, +}); diff --git a/src/handlers/modules/starter.ts b/src/handlers/modules/starter.ts new file mode 100644 index 00000000..a9f47131 --- /dev/null +++ b/src/handlers/modules/starter.ts @@ -0,0 +1,35 @@ +import { setSchedules } from "~/core/docker/scheduler"; +import { pluginManager } from "~/core/plugins/plugin-manager"; +import { startDockerStatsBroadcast } from "./docker-socket"; + +function banner(msg: string) { + const fenced = `= ${msg} =`; + const lines = msg.length; + console.info("=".repeat(fenced.length)); + console.info(fenced); + console.info("=".repeat(fenced.length)); +} + +class starter { + public started = false; + async startAll() { + try { + if (!this.started) { + banner("Setting schedules"); + await setSchedules(); + banner("Importing plugins"); + await startDockerStatsBroadcast(); + banner("Started DockStatAPI succesfully"); + await pluginManager.start(); + banner("Starting WebSocket server"); + this.started = true; + return; + } + console.info("Already started"); + } catch (error) { + throw new Error(`Could not start DockStatAPI: ${error}`); + } + } +} + +export const Starter = new starter(); diff --git a/src/handlers/sockets.ts b/src/handlers/sockets.ts index d176ea5f..f7011a88 100644 --- a/src/handlers/sockets.ts +++ b/src/handlers/sockets.ts @@ -1,9 +1,11 @@ -import { createDockerStatsStream } from "./modules/docker-socket"; import { createStackStream } from "./modules/live-stacks"; import { createLogStream } from "./modules/logs-socket"; export const Sockets = { - createDockerStatsStream, + stats: { + port: 4837, + path: "/ws/docker", + }, createLogStream, createStackStream, }; diff --git a/tsconfig.json b/tsconfig.json index 9847d57d..b0ce6926 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,7 +30,7 @@ "module": "ES2022" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ "moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */, "paths": { "~/*": ["./src/*"], "~/typings/*": ["./typings/*"] diff --git a/typings b/typings index 2c22e7f3..ca039be0 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 2c22e7f3fff362939c79291a097ee5b13d700a79 +Subproject commit ca039be0adea6274850c016c64595ee907a8ba3f From 082bfa840029c168eb0905a85713abaa3bc73328 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 6 Jul 2025 16:57:52 +0200 Subject: [PATCH 350/369] I don't even know anymore man --- src/core/stacks/controller.ts | 99 +++++++----- src/core/stacks/operations/runStackCommand.ts | 53 +++++-- src/core/utils/logger.ts | 5 +- src/handlers/index.ts | 2 +- src/handlers/modules/docker-socket.ts | 148 ++++++++++++------ src/handlers/modules/live-stacks.ts | 12 -- src/handlers/sockets.ts | 6 +- 7 files changed, 202 insertions(+), 123 deletions(-) diff --git a/src/core/stacks/controller.ts b/src/core/stacks/controller.ts index 399c946d..0b0b5174 100644 --- a/src/core/stacks/controller.ts +++ b/src/core/stacks/controller.ts @@ -2,10 +2,10 @@ import { rm } from "node:fs/promises"; import DockerCompose from "docker-compose"; import { dbFunctions } from "~/core/database"; import { logger } from "~/core/utils/logger"; -import { postToClient } from "~/handlers/modules/live-stacks"; import type { stacks_config } from "~/typings/database"; import type { Stack } from "~/typings/docker-compose"; import type { ComposeSpec } from "~/typings/docker-compose"; +import { broadcast } from "../../handlers/modules/docker-socket"; import { checkStacks } from "./checker"; import { runStackCommand } from "./operations/runStackCommand"; import { wrapProgressCallback } from "./operations/runStackCommand"; @@ -33,13 +33,17 @@ export async function deployStack(stack_config: stacks_config): Promise { throw new Error("Failed to add stack to database"); } - postToClient({ - type: "stack-status", - timestamp: new Date(), + // Broadcast pending status + broadcast({ + topic: "stack", data: { - stack_id: stackId, - status: "pending", - message: "Creating stack configuration", + timestamp: new Date(), + type: "stack-status", + data: { + stack_id: stackId, + status: "pending", + message: "Creating stack configuration", + }, }, }); @@ -65,13 +69,17 @@ export async function deployStack(stack_config: stacks_config): Promise { "deploying", ); - postToClient({ - type: "stack-status", - timestamp: new Date(), + // Broadcast deployed status + broadcast({ + topic: "stack", data: { - stack_id: stackId, - status: "deployed", - message: "Stack deployed successfully", + timestamp: new Date(), + type: "stack-status", + data: { + stack_id: stackId, + status: "deployed", + message: "Stack deployed successfully", + }, }, }); @@ -109,14 +117,17 @@ export async function deployStack(stack_config: stacks_config): Promise { } } - postToClient({ - type: "stack-error", - timestamp: new Date(), + // Broadcast deployment error + broadcast({ + topic: "stack", data: { - stack_id: stackId ?? 0, - action: "deploying", - message: errorMsg, - timestamp: new Date().toISOString(), + timestamp: new Date(), + type: "stack-error", + data: { + stack_id: stackId ?? 0, + action: "deploying", + message: errorMsg, + }, }, }); throw new Error(errorMsg); @@ -211,14 +222,17 @@ export async function removeStack(stack_id: number): Promise { } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error(errorMsg); - postToClient({ - type: "stack-error", - timestamp: new Date(), + // Broadcast removal error + broadcast({ + topic: "stack", data: { - stack_id, - action: "removing", - message: `Directory removal failed: ${errorMsg}`, - timestamp: new Date().toISOString(), + timestamp: new Date(), + type: "stack-error", + data: { + stack_id, + action: "removing", + message: `Directory removal failed: ${errorMsg}`, + }, }, }); throw new Error(errorMsg); @@ -226,25 +240,32 @@ export async function removeStack(stack_id: number): Promise { dbFunctions.deleteStack(stack_id); - postToClient({ - type: "stack-removed", - timestamp: new Date(), + // Broadcast successful removal + broadcast({ + topic: "stack", data: { - stack_id, - message: "Stack removed successfully", + timestamp: new Date(), + type: "stack-removed", + data: { + stack_id, + message: "Stack removed successfully", + }, }, }); } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error(errorMsg); - postToClient({ - type: "stack-error", - timestamp: new Date(), + // Broadcast removal error + broadcast({ + topic: "stack", data: { - stack_id, - action: "removing", - message: errorMsg, - timestamp: new Date().toISOString(), + timestamp: new Date(), + type: "stack-error", + data: { + stack_id, + action: "removing", + message: errorMsg, + }, }, }); throw new Error(errorMsg); diff --git a/src/core/stacks/operations/runStackCommand.ts b/src/core/stacks/operations/runStackCommand.ts index 34ac112a..d613a7c7 100644 --- a/src/core/stacks/operations/runStackCommand.ts +++ b/src/core/stacks/operations/runStackCommand.ts @@ -1,6 +1,6 @@ import { logger } from "~/core/utils/logger"; -import { postToClient } from "~/handlers/modules/live-stacks"; import type { Stack } from "~/typings/docker-compose"; +import { broadcast } from "../../../handlers/modules/docker-socket"; import { getStackName, getStackPath } from "./stackHelpers"; export function wrapProgressCallback(progressCallback?: (log: string) => void) { @@ -49,14 +49,17 @@ export async function runStackCommand( } } - postToClient({ - type: "stack-progress", - timestamp: new Date(), + // Broadcast progress + broadcast({ + topic: "stack", data: { - stack_id, - action, - message, - timestamp: new Date().toISOString(), + timestamp: new Date(), + type: "stack-progress", + data: { + stack_id, + message, + action, + }, }, }); }; @@ -69,6 +72,21 @@ export async function runStackCommand( `Successfully completed command for stack_id=${stack_id}, action="${action}"`, ); + // Optionally broadcast status on completion + broadcast({ + topic: "stack", + data: { + timestamp: new Date(), + type: "stack-status", + data: { + stack_id, + status: "completed", + message: `Completed ${action}`, + action, + }, + }, + }); + return result; } catch (error: unknown) { const errorMsg = @@ -76,16 +94,21 @@ export async function runStackCommand( logger.debug( `Error occurred for stack_id=${stack_id}, action="${action}": ${errorMsg}`, ); - postToClient({ - type: "stack-error", - timestamp: new Date(), + + // Broadcast error + broadcast({ + topic: "stack", data: { - stack_id, - action, - message: errorMsg, - timestamp: new Date().toISOString(), + timestamp: new Date(), + type: "stack-error", + data: { + stack_id, + action, + message: errorMsg, + }, }, }); + throw new Error(`Error while ${action} stack "${stack_id}": ${errorMsg}`); } } diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index 3b9248d2..f00deb4e 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -1,5 +1,6 @@ import path from "node:path"; -import chalk, { type ChalkFunction } from "chalk"; +import chalk from "chalk"; +import type { ChalkInstance } from "chalk"; import type { TransformableInfo } from "logform"; import { createLogger, format, transports } from "winston"; import wrapAnsi from "wrap-ansi"; @@ -53,7 +54,7 @@ const formatTerminalMessage = (message: string, prefix: string): string => { } }; -const levelColors: Record = { +const levelColors: Record = { error: chalk.red.bold, warn: chalk.yellow.bold, info: chalk.green.bold, diff --git a/src/handlers/index.ts b/src/handlers/index.ts index f5360a2b..c8d8e7bb 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -18,7 +18,7 @@ export const handlers = { StackHandler, LogHandler, CheckHealth, - Sockets: Sockets, + Socket: "ws://localhost:4837/ws", }; Starter.startAll(); diff --git a/src/handlers/modules/docker-socket.ts b/src/handlers/modules/docker-socket.ts index f78eb8d5..1a20c9ce 100644 --- a/src/handlers/modules/docker-socket.ts +++ b/src/handlers/modules/docker-socket.ts @@ -7,40 +7,47 @@ import { calculateMemoryUsage, } from "~/core/utils/calculations"; import { logger } from "~/core/utils/logger"; -import type { DockerStatsEvent } from "~/typings/docker"; +import type { log_message } from "~/typings/database"; +import type { DockerHost } from "~/typings/docker"; +import type { WSMessage } from "~/typings/websocket"; +import { createLogStream } from "./logs-socket"; -// Track all connected WebSocket clients +// Unified WebSocket message with topic for client-side routing const clients = new Set>(); -// Broadcast a DockerStatsEvent to every connected client -function broadcast(event: DockerStatsEvent) { - const message = JSON.stringify(event); +/** + * Broadcasts a WSMessage to all connected clients. + */ +export function broadcast(wsMsg: WSMessage) { + const payload = JSON.stringify(wsMsg); for (const ws of clients) { if (ws.readyState === 1) { - ws.send(message); + ws.send(payload); } } } -// Start Docker stats polling and broadcasting +/** + * Streams Docker stats for all hosts and broadcasts events. + */ export async function startDockerStatsBroadcast() { logger.debug("Starting Docker stats broadcast..."); try { - const hosts = dbFunctions.getDockerHosts(); + const hosts: DockerHost[] = dbFunctions.getDockerHosts(); logger.debug(`Retrieved ${hosts.length} Docker host(s)`); for (const host of hosts) { try { const docker = getDockerClient(host); await docker.ping(); + const containers = await docker.listContainers({ all: true }); logger.debug( `Host ${host.name} contains ${containers.length} containers`, ); for (const info of containers) { - // Kick off one independent async task per container (async () => { try { const statsStream = await docker @@ -53,75 +60,116 @@ export async function startDockerStatsBroadcast() { if (!line) continue; try { const stats = JSON.parse(line); - broadcast({ - type: "stats", - id: info.Id, - hostId: host.id, - name: info.Names[0].replace(/^\//, ""), - image: info.Image, - status: info.Status, - state: stats.state || info.State, - cpuUsage: calculateCpuPercent(stats) ?? 0, - memoryUsage: calculateMemoryUsage(stats) ?? 0, - }); + const msg: WSMessage = { + topic: "stats", + data: { + id: info.Id, + hostId: host.id, + name: info.Names[0].replace(/^\//, ""), + image: info.Image, + status: info.Status, + state: stats.state || info.State, + cpuUsage: calculateCpuPercent(stats) ?? 0, + memoryUsage: calculateMemoryUsage(stats) ?? 0, + }, + }; + broadcast(msg); } catch (err) { - broadcast({ - type: "error", - hostId: host.id, - containerId: info.Id, - error: `Parse error: ${(err as Error).message}`, - }); + const errorMsg = (err as Error).message; + const msg: WSMessage = { + topic: "error", + data: { + hostId: host.id, + containerId: info.Id, + error: `Parse error: ${errorMsg}`, + }, + }; + broadcast(msg); } } } catch (err) { - broadcast({ - type: "error", - hostId: host.id, - containerId: info.Id, - error: `Stats stream error: ${(err as Error).message}`, - }); + const errorMsg = (err as Error).message; + const msg: WSMessage = { + topic: "error", + data: { + hostId: host.id, + containerId: info.Id, + error: `Stats stream error: ${errorMsg}`, + }, + }; + broadcast(msg); } })(); } } catch (err) { - broadcast({ - type: "error", - hostId: host.id, - error: `Host connection error: ${(err as Error).message}`, - }); + const errorMsg = (err as Error).message; + const msg: WSMessage = { + topic: "error", + data: { + hostId: host.id, + error: `Host connection error: ${errorMsg}`, + }, + }; + broadcast(msg); } } } catch (err) { - broadcast({ - type: "error", - hostId: 0, - error: `Initialization error: ${(err as Error).message}`, - }); + const errorMsg = (err as Error).message; + const msg: WSMessage = { + topic: "error", + data: { + hostId: 0, + error: `Initialization error: ${errorMsg}`, + }, + }; + broadcast(msg); } } -serve({ +/** + * Sets up a log stream to forward application logs over WebSocket. + */ +function startLogBroadcast() { + const logStream = createLogStream(); + logStream.on("data", (chunk: log_message) => { + const msg: WSMessage = { + topic: "logs", + data: chunk, + }; + broadcast(msg); + }); +} + +/** + * WebSocket server serving multiple topics over one socket. + */ +export const WSServer = serve({ port: 4837, reusePort: true, fetch(req, server) { - // Upgrade requests to WebSocket - if (req.url.endsWith("/ws/docker")) { - if (server.upgrade(req)) { - return; // auto 101 Switching Protocols - } + //if (req.url.endsWith("/ws")) { + if (server.upgrade(req)) { + logger.debug("Upgraded!"); + return; } + //} return new Response("Expected WebSocket upgrade", { status: 426 }); }, - websocket: { open(ws) { logger.debug("Client connected via WebSocket"); clients.add(ws); }, + message() {}, close(ws, code, reason) { logger.debug(`Client disconnected (${code}): ${reason}`); clients.delete(ws); }, - message() {}, }, }); + +// Initialize broadcasts +startDockerStatsBroadcast().catch((err) => { + logger.error("Failed to start Docker stats broadcast:", err); +}); +startLogBroadcast(); diff --git a/src/handlers/modules/live-stacks.ts b/src/handlers/modules/live-stacks.ts index 924d76ab..ab26ccfd 100644 --- a/src/handlers/modules/live-stacks.ts +++ b/src/handlers/modules/live-stacks.ts @@ -1,6 +1,5 @@ import { PassThrough, type Readable } from "node:stream"; import { logger } from "~/core/utils/logger"; -import type { stackSocketMessage } from "~/typings/websocket"; const activeStreams = new Set(); @@ -30,14 +29,3 @@ export function createStackStream(): Readable { return stream; } - -export function postToClient(stackMessage: stackSocketMessage) { - for (const stream of activeStreams) { - try { - stream.push(JSON.stringify(stackMessage)); - } catch (error) { - activeStreams.delete(stream); - logger.error("Failed to send to Socket:", error); - } - } -} diff --git a/src/handlers/sockets.ts b/src/handlers/sockets.ts index f7011a88..ff463c6c 100644 --- a/src/handlers/sockets.ts +++ b/src/handlers/sockets.ts @@ -1,11 +1,9 @@ +import { WSServer } from "./modules/docker-socket"; import { createStackStream } from "./modules/live-stacks"; import { createLogStream } from "./modules/logs-socket"; export const Sockets = { - stats: { - port: 4837, - path: "/ws/docker", - }, + dockerStatsStream: `${WSServer.hostname}${WSServer.port}/ws`, createLogStream, createStackStream, }; From 84d7c766c4cadbf1cc2036ca0e286b77ccfb3d49 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 6 Jul 2025 14:59:03 +0000 Subject: [PATCH 351/369] Update dependency graphs --- dependency-graph.mmd | 196 +++--- dependency-graph.svg | 1385 +++++++++++++++++++++++------------------- 2 files changed, 860 insertions(+), 721 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index aaacb834..6e348fe4 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -13,15 +13,16 @@ subgraph 2["handlers"] 4["config.ts"] subgraph P["modules"] Q["logs-socket.ts"] -1B["docker-socket.ts"] -1D["live-stacks.ts"] +1C["docker-socket.ts"] +1F["starter.ts"] +1K["live-stacks.ts"] end -14["database.ts"] -15["docker.ts"] -19["logs.ts"] -1A["sockets.ts"] -1F["stacks.ts"] -1O["utils.ts"] +16["database.ts"] +17["docker.ts"] +1B["logs.ts"] +1J["sockets.ts"] +1L["stacks.ts"] +1U["utils.ts"] end subgraph B["core"] subgraph C["database"] @@ -40,25 +41,27 @@ end subgraph N["utils"] O["logger.ts"] Y["helpers.ts"] -12["package-json.ts"] -1C["calculations.ts"] +13["change-me-checker.ts"] +14["package-json.ts"] +1E["calculations.ts"] end subgraph Z["plugins"] 10["plugin-manager.ts"] +12["loader.ts"] end -subgraph 17["docker"] -18["client.ts"] -1P["scheduler.ts"] -1Q["store-container-stats.ts"] -1R["store-host-stats.ts"] +subgraph 19["docker"] +1A["client.ts"] +1G["scheduler.ts"] +1H["store-container-stats.ts"] +1I["store-host-stats.ts"] end -subgraph 1G["stacks"] -1H["controller.ts"] -1J["checker.ts"] -subgraph 1K["operations"] -1L["runStackCommand.ts"] -1M["stackHelpers.ts"] -1N["stackStatus.ts"] +subgraph 1M["stacks"] +1N["controller.ts"] +1P["checker.ts"] +subgraph 1Q["operations"] +1R["runStackCommand.ts"] +1S["stackHelpers.ts"] +1T["stackStatus.ts"] end end end @@ -69,9 +72,9 @@ subgraph 6["typings"] 8["docker"] 9["plugin"] F["misc"] -16["dockerode"] -1E["websocket"] -1I["docker-compose"] +18["dockerode"] +1D["websocket"] +1O["docker-compose"] end end subgraph A["fs"] @@ -82,21 +85,25 @@ K["os"] L["path"] R["stream"] 11["events"] -13["package.json"] +15["package.json"] 1-->3 3-->4 -3-->14 -3-->15 -3-->19 -3-->1A +3-->16 +3-->17 +3-->1B +3-->1C 3-->1F -3-->1O -3-->1P +3-->1J +3-->1L +3-->1U +3-->1G +3-->10 +3-->O 4-->D 4-->E 4-->10 4-->O -4-->12 +4-->14 4-->7 4-->8 4-->9 @@ -150,74 +157,85 @@ X-->M X-->7 Y-->O 10-->O +10-->12 10-->8 10-->9 10-->11 12-->13 -14-->D -15-->D -15-->18 -15-->Y -15-->O -15-->8 -15-->16 -18-->O -18-->8 -19-->D -19-->O -1A-->1B -1A-->1D -1A-->Q +12-->O +12-->10 +12-->A +12-->L +13-->O +13-->J +14-->15 +16-->D +17-->D +17-->1A +17-->O +17-->8 +17-->18 +1A-->O +1A-->8 1B-->D -1B-->18 -1B-->1C 1B-->O -1B-->8 -1B-->R -1D-->O -1D-->1E -1D-->R -1F-->D -1F-->1H -1F-->O -1F-->7 -1H-->1J -1H-->1L -1H-->1M -1H-->1N -1H-->D +1C-->Q +1C-->D +1C-->1A +1C-->1E +1C-->O +1C-->7 +1C-->8 +1C-->1D +1F-->1C +1F-->1G +1F-->10 +1G-->D +1G-->1H +1G-->1I +1G-->O +1G-->7 1H-->O -1H-->1D +1H-->D +1H-->1A +1H-->1E 1H-->7 -1H-->1I -1H-->J -1J-->D -1J-->O -1L-->1M +1I-->D +1I-->1A +1I-->O +1I-->8 +1I-->18 +1J-->1C +1J-->1K +1J-->Q +1K-->O +1K-->R +1L-->D +1L-->1N 1L-->O -1L-->1D -1L-->1I -1M-->D -1M-->Y -1M-->O -1M-->1I -1N-->1L +1L-->7 +1N-->1C +1N-->1P +1N-->1R +1N-->1S +1N-->1T 1N-->D 1N-->O -1O-->O +1N-->7 +1N-->1O +1N-->J 1P-->D -1P-->1Q -1P-->1R 1P-->O -1P-->7 -1Q-->O -1Q-->D -1Q-->18 -1Q-->1C -1Q-->7 -1R-->D -1R-->18 +1R-->1C +1R-->1S 1R-->O -1R-->8 -1R-->16 +1R-->1O +1S-->D +1S-->Y +1S-->O +1S-->1O +1T-->1R +1T-->D +1T-->O +1U-->O diff --git a/dependency-graph.svg b/dependency-graph.svg index 024d4c75..842fbcdc 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,82 +4,82 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/stacks/operations - -operations + +operations cluster_src/core/utils - -utils + +utils cluster_src/handlers - -handlers + +handlers cluster_src/handlers/modules - -modules + +modules cluster_~ - -~ + +~ cluster_~/typings - -typings + +typings bun:sqlite - -bun:sqlite + +bun:sqlite @@ -87,8 +87,8 @@ events - -events + +events @@ -96,8 +96,8 @@ fs - -fs + +fs @@ -105,8 +105,8 @@ fs/promises - -promises + +promises @@ -114,8 +114,8 @@ os - -os + +os @@ -123,8 +123,8 @@ package.json - -package.json + +package.json @@ -132,8 +132,8 @@ path - -path + +path @@ -141,8 +141,8 @@ src/core/database/_dbState.ts - -_dbState.ts + +_dbState.ts @@ -150,1261 +150,1382 @@ src/core/database/backup.ts - -backup.ts + +backup.ts src/core/database/backup.ts->fs - - + + src/core/database/backup.ts->src/core/database/_dbState.ts - - + + src/core/database/database.ts - -database.ts + +database.ts src/core/database/backup.ts->src/core/database/database.ts - - + + src/core/database/helper.ts - -helper.ts + +helper.ts src/core/database/backup.ts->src/core/database/helper.ts - - - - + + + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + ~/typings/misc - -misc + +misc src/core/database/backup.ts->~/typings/misc - - + + src/core/database/database.ts->bun:sqlite - - + + src/core/database/database.ts->fs - - + + src/core/database/database.ts->fs/promises - - + + src/core/database/database.ts->os - - + + src/core/database/database.ts->path - - + + src/core/database/helper.ts->src/core/database/_dbState.ts - - + + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + - + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + ~/typings/database - -database + +database - + src/core/utils/logger.ts->~/typings/database - - + + src/core/database/index.ts - -index.ts + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + - + src/handlers/modules/logs-socket.ts - - -logs-socket.ts + + +logs-socket.ts - + src/core/utils/logger.ts->src/handlers/modules/logs-socket.ts - - - - + + + + src/core/database/config.ts - -config.ts + +config.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts->~/typings/database - - + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + ~/typings/docker - -docker + +docker src/core/database/dockerHosts.ts->~/typings/docker - - + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts->~/typings/docker - - + + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + src/core/database/logs.ts->~/typings/database - - + + src/core/database/stacks.ts->src/core/database/database.ts - - + + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + src/core/database/stacks.ts->~/typings/database - - + + src/core/utils/helpers.ts - -helpers.ts + +helpers.ts src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - + + + + - + src/core/utils/helpers.ts->src/core/utils/logger.ts - - - - + + + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->~/typings/docker - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->~/typings/database - - + + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-container-stats.ts->~/typings/database - - + + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->~/typings/docker - - + + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + ~/typings/dockerode - -dockerode + +dockerode src/core/docker/store-host-stats.ts->~/typings/dockerode - - + + - + +src/core/plugins/loader.ts + + +loader.ts + + + + + +src/core/plugins/loader.ts->fs + + + + + +src/core/plugins/loader.ts->path + + + + + +src/core/plugins/loader.ts->src/core/utils/logger.ts + + + + + +src/core/utils/change-me-checker.ts + + +change-me-checker.ts + + + + + +src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts + + + + + src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +plugin-manager.ts + + +src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts + + + + + + + +src/core/utils/change-me-checker.ts->fs/promises + + + + + +src/core/utils/change-me-checker.ts->src/core/utils/logger.ts + + + - + src/core/plugins/plugin-manager.ts->events - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->~/typings/docker - - + + + + + +src/core/plugins/plugin-manager.ts->src/core/plugins/loader.ts + + + + - + ~/typings/plugin - - -plugin + + +plugin - + src/core/plugins/plugin-manager.ts->~/typings/plugin - - + + - + src/core/stacks/checker.ts - - -checker.ts + + +checker.ts - + src/core/stacks/checker.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/checker.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->~/typings/database - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts->src/core/stacks/checker.ts - - + + + + + +src/handlers/modules/docker-socket.ts + + +docker-socket.ts + + + + + +src/core/stacks/controller.ts->src/handlers/modules/docker-socket.ts + + - + src/core/stacks/operations/runStackCommand.ts - - -runStackCommand.ts + + +runStackCommand.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/runStackCommand.ts - - + + - + src/core/stacks/operations/stackHelpers.ts - - -stackHelpers.ts + + +stackHelpers.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/stackHelpers.ts - - + + - + src/core/stacks/operations/stackStatus.ts - - -stackStatus.ts + + +stackStatus.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/stackStatus.ts - - - - - -src/handlers/modules/live-stacks.ts - - -live-stacks.ts - - - - - -src/core/stacks/controller.ts->src/handlers/modules/live-stacks.ts - - + + - + ~/typings/docker-compose - - -docker-compose + + +docker-compose - + src/core/stacks/controller.ts->~/typings/docker-compose - - + + + + + +src/handlers/modules/docker-socket.ts->src/core/utils/logger.ts + + + + + +src/handlers/modules/docker-socket.ts->~/typings/database + + + + + +src/handlers/modules/docker-socket.ts->~/typings/docker + + + + + +src/handlers/modules/docker-socket.ts->src/core/database/index.ts + + + + + +src/handlers/modules/docker-socket.ts->src/core/docker/client.ts + + + + + +src/handlers/modules/docker-socket.ts->src/core/utils/calculations.ts + + + + + +src/handlers/modules/docker-socket.ts->src/handlers/modules/logs-socket.ts + + + + + +~/typings/websocket + + +websocket + + + + + +src/handlers/modules/docker-socket.ts->~/typings/websocket + + - + src/core/stacks/operations/runStackCommand.ts->src/core/utils/logger.ts - - + + + + + +src/core/stacks/operations/runStackCommand.ts->src/handlers/modules/docker-socket.ts + + - + src/core/stacks/operations/runStackCommand.ts->src/core/stacks/operations/stackHelpers.ts - - - - - -src/core/stacks/operations/runStackCommand.ts->src/handlers/modules/live-stacks.ts - - + + - + src/core/stacks/operations/runStackCommand.ts->~/typings/docker-compose - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/database/index.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/utils/helpers.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->~/typings/docker-compose - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/database/index.ts - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/stacks/operations/runStackCommand.ts - - - - - -src/handlers/modules/live-stacks.ts->src/core/utils/logger.ts - - - - - -stream - - -stream - - - - - -src/handlers/modules/live-stacks.ts->stream - - - - - -~/typings/websocket - - -websocket - - - - - -src/handlers/modules/live-stacks.ts->~/typings/websocket - - + + - + src/handlers/modules/logs-socket.ts->src/core/utils/logger.ts - - - - + + + + - + src/handlers/modules/logs-socket.ts->~/typings/database - - + + + + + +stream + + +stream + + - + src/handlers/modules/logs-socket.ts->stream - - + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - + src/handlers/config.ts - - -config.ts + + +config.ts - + src/handlers/config.ts->fs - - + + - + src/handlers/config.ts->src/core/database/backup.ts - - + + - + src/handlers/config.ts->src/core/utils/logger.ts - - + + - + src/handlers/config.ts->~/typings/database - - + + - + src/handlers/config.ts->~/typings/docker - - + + - + src/handlers/config.ts->src/core/database/index.ts - - + + - + src/handlers/config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/handlers/config.ts->~/typings/plugin - - + + - + src/handlers/config.ts->src/core/utils/package-json.ts - - + + - + src/handlers/database.ts - - -database.ts + + +database.ts - + src/handlers/database.ts->src/core/database/index.ts - - + + - + src/handlers/docker.ts - - -docker.ts + + +docker.ts - + src/handlers/docker.ts->src/core/utils/logger.ts - - + + - + src/handlers/docker.ts->~/typings/docker - - + + - + src/handlers/docker.ts->src/core/database/index.ts - - - - - -src/handlers/docker.ts->src/core/utils/helpers.ts - - + + - + src/handlers/docker.ts->src/core/docker/client.ts - - + + - + src/handlers/docker.ts->~/typings/dockerode - - + + - + src/handlers/index.ts - - -index.ts + + +index.ts + + +src/handlers/index.ts->src/core/utils/logger.ts + + + - + src/handlers/index.ts->src/core/docker/scheduler.ts - - + + + + + +src/handlers/index.ts->src/core/plugins/plugin-manager.ts + + + + + +src/handlers/index.ts->src/handlers/modules/docker-socket.ts + + - + src/handlers/index.ts->src/handlers/config.ts - - + + - + src/handlers/index.ts->src/handlers/database.ts - - + + - + src/handlers/index.ts->src/handlers/docker.ts - - + + - + src/handlers/logs.ts - - -logs.ts + + +logs.ts - + src/handlers/index.ts->src/handlers/logs.ts - - + + + + + +src/handlers/modules/starter.ts + + +starter.ts + + + + + +src/handlers/index.ts->src/handlers/modules/starter.ts + + - + src/handlers/sockets.ts - - -sockets.ts + + +sockets.ts - + src/handlers/index.ts->src/handlers/sockets.ts - - + + - + src/handlers/stacks.ts - - -stacks.ts + + +stacks.ts - + src/handlers/index.ts->src/handlers/stacks.ts - - + + - + src/handlers/utils.ts - - -utils.ts + + +utils.ts - + src/handlers/index.ts->src/handlers/utils.ts - - + + - + src/handlers/logs.ts->src/core/utils/logger.ts - - + + - + src/handlers/logs.ts->src/core/database/index.ts - - + + + + + +src/handlers/modules/starter.ts->src/core/docker/scheduler.ts + + + + + +src/handlers/modules/starter.ts->src/core/plugins/plugin-manager.ts + + + + + +src/handlers/modules/starter.ts->src/handlers/modules/docker-socket.ts + + - - -src/handlers/sockets.ts->src/handlers/modules/live-stacks.ts - - + + +src/handlers/sockets.ts->src/handlers/modules/docker-socket.ts + + - + src/handlers/sockets.ts->src/handlers/modules/logs-socket.ts - - + + - - -src/handlers/modules/docker-socket.ts - - -docker-socket.ts + + +src/handlers/modules/live-stacks.ts + + +live-stacks.ts - - -src/handlers/sockets.ts->src/handlers/modules/docker-socket.ts - - + + +src/handlers/sockets.ts->src/handlers/modules/live-stacks.ts + + - + src/handlers/stacks.ts->src/core/utils/logger.ts - - + + - + src/handlers/stacks.ts->~/typings/database - - + + - + src/handlers/stacks.ts->src/core/database/index.ts - - + + - + src/handlers/stacks.ts->src/core/stacks/controller.ts - - + + - + src/handlers/utils.ts->src/core/utils/logger.ts - - - - - -src/handlers/modules/docker-socket.ts->src/core/utils/logger.ts - - + + - - -src/handlers/modules/docker-socket.ts->~/typings/docker - - - - - -src/handlers/modules/docker-socket.ts->src/core/database/index.ts - - - - - -src/handlers/modules/docker-socket.ts->src/core/docker/client.ts - - - - - -src/handlers/modules/docker-socket.ts->src/core/utils/calculations.ts - - + + +src/handlers/modules/live-stacks.ts->src/core/utils/logger.ts + + - - -src/handlers/modules/docker-socket.ts->stream - - + + +src/handlers/modules/live-stacks.ts->stream + + - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/handlers/index.ts - - + + From 3c4a0c3e99c96fc5b65b84f5998afca3a124258e Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Mon, 7 Jul 2025 13:25:09 +0200 Subject: [PATCH 352/369] Minor changes --- src/core/utils/logger.ts | 330 +++++++++++++++++++-------------------- 1 file changed, 165 insertions(+), 165 deletions(-) diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index f00deb4e..d3b891e1 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -7,7 +7,7 @@ import wrapAnsi from "wrap-ansi"; import { dbFunctions } from "~/core/database"; -import { logToClients } from "~/handlers/modules/logs-socket"; +import { logToClients } from "../../handlers/modules/logs-socket"; import type { log_message } from "~/typings/database"; @@ -16,188 +16,188 @@ import { backupInProgress } from "../database/_dbState"; const padNewlines = true; //process.env.PAD_NEW_LINES !== "false"; type LogLevel = - | "error" - | "warn" - | "info" - | "debug" - | "verbose" - | "silly" - | "task" - | "ut"; + | "error" + | "warn" + | "info" + | "debug" + | "verbose" + | "silly" + | "task" + | "ut"; // biome-ignore lint/suspicious/noControlCharactersInRegex: const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; const formatTerminalMessage = (message: string, prefix: string): string => { - try { - const cleanPrefix = prefix.replace(ansiRegex, ""); - const maxWidth = process.stdout.columns || 80; - const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); - - if (!padNewlines) return message; - - const wrapped = wrapAnsi(message, wrapWidth, { - trim: true, - hard: true, - wordWrap: true, - }); - - return wrapped - .split("\n") - .map((line, index) => { - return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; - }) - .join("\n"); - } catch (error) { - console.error("Error formatting terminal message:", error); - return message; - } + try { + const cleanPrefix = prefix.replace(ansiRegex, ""); + const maxWidth = process.stdout.columns || 80; + const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); + + if (!padNewlines) return message; + + const wrapped = wrapAnsi(message, wrapWidth, { + trim: true, + hard: true, + wordWrap: true, + }); + + return wrapped + .split("\n") + .map((line, index) => { + return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; + }) + .join("\n"); + } catch (error) { + console.error("Error formatting terminal message:", error); + return message; + } }; const levelColors: Record = { - error: chalk.red.bold, - warn: chalk.yellow.bold, - info: chalk.green.bold, - debug: chalk.blue.bold, - verbose: chalk.cyan.bold, - silly: chalk.magenta.bold, - task: chalk.cyan.bold, - ut: chalk.hex("#9D00FF"), + error: chalk.red.bold, + warn: chalk.yellow.bold, + info: chalk.green.bold, + debug: chalk.blue.bold, + verbose: chalk.cyan.bold, + silly: chalk.magenta.bold, + task: chalk.cyan.bold, + ut: chalk.hex("#9D00FF"), }; const parseTimestamp = (timestamp: string): string => { - const [datePart, timePart] = timestamp.split(" "); - const [day, month] = datePart.split("/"); - const [hours, minutes, seconds] = timePart.split(":"); - const year = new Date().getFullYear(); - const date = new Date( - year, - Number.parseInt(month) - 1, - Number.parseInt(day), - Number.parseInt(hours), - Number.parseInt(minutes), - Number.parseInt(seconds), - ); - return date.toISOString(); + const [datePart, timePart] = timestamp.split(" "); + const [day, month] = datePart.split("/"); + const [hours, minutes, seconds] = timePart.split(":"); + const year = new Date().getFullYear(); + const date = new Date( + year, + Number.parseInt(month) - 1, + Number.parseInt(day), + Number.parseInt(hours), + Number.parseInt(minutes), + Number.parseInt(seconds) + ); + return date.toISOString(); }; const handleWebSocketLog = (log: log_message) => { - try { - logToClients({ - ...log, - timestamp: parseTimestamp(log.timestamp), - }); - } catch (error) { - console.error( - `WebSocket logging failed: ${ - error instanceof Error ? error.message : error - }`, - ); - } + try { + logToClients({ + ...log, + timestamp: parseTimestamp(log.timestamp), + }); + } catch (error) { + console.error( + `WebSocket logging failed: ${ + error instanceof Error ? error.message : error + }` + ); + } }; const handleDatabaseLog = (log: log_message): void => { - if (backupInProgress) { - return; - } - try { - dbFunctions.addLogEntry({ - ...log, - timestamp: parseTimestamp(log.timestamp), - }); - } catch (error) { - console.error( - `Database logging failed: ${ - error instanceof Error ? error.message : error - }`, - ); - } + if (backupInProgress) { + return; + } + try { + dbFunctions.addLogEntry({ + ...log, + timestamp: parseTimestamp(log.timestamp), + }); + } catch (error) { + console.error( + `Database logging failed: ${ + error instanceof Error ? error.message : error + }` + ); + } }; export const logger = createLogger({ - level: process.env.LOG_LEVEL || "debug", - format: format.combine( - format.timestamp({ format: "DD/MM HH:mm:ss" }), - format((info) => { - const stack = new Error().stack?.split("\n"); - let file = "unknown"; - let line = 0; - - if (stack) { - for (let i = 2; i < stack.length; i++) { - const lineStr = stack[i].trim(); - if ( - !lineStr.includes("node_modules") && - !lineStr.includes(path.basename(import.meta.url)) - ) { - const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); - if (matches) { - file = path.basename(matches[1]); - line = Number.parseInt(matches[2], 10); - break; - } - } - } - } - return { ...info, file, line }; - })(), - format.printf((info) => { - const { timestamp, level, message, file, line } = - info as TransformableInfo & log_message; - let processedLevel = level as LogLevel; - let processedMessage = String(message); - - if (processedMessage.startsWith("__task__")) { - processedMessage = processedMessage - .replace(/__task__/g, "") - .trimStart(); - processedLevel = "task"; - if (processedMessage.startsWith("__db__")) { - processedMessage = processedMessage - .replace(/__db__/g, "") - .trimStart(); - processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; - } - } else if (processedMessage.startsWith("__UT__")) { - processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); - processedLevel = "ut"; - } - - if (file.endsWith("plugin.ts")) { - processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; - } - - const paddedLevel = processedLevel.toUpperCase().padEnd(5); - const coloredLevel = (levelColors[processedLevel] || chalk.white)( - paddedLevel, - ); - const coloredContext = chalk.cyan(`${file}:${line}`); - const coloredTimestamp = chalk.yellow(timestamp); - - const prefix = `${paddedLevel} [ ${timestamp} ] - `; - const combinedContent = `${processedMessage} - ${coloredContext}`; - - const formattedMessage = padNewlines - ? formatTerminalMessage(combinedContent, prefix) - : combinedContent; - - handleDatabaseLog({ - level: processedLevel, - timestamp: timestamp, - message: processedMessage, - file: file, - line: line, - }); - handleWebSocketLog({ - level: processedLevel, - timestamp: timestamp, - message: processedMessage, - file: file, - line: line, - }); - - return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; - }), - ), - transports: [new transports.Console()], + level: process.env.LOG_LEVEL || "debug", + format: format.combine( + format.timestamp({ format: "DD/MM HH:mm:ss" }), + format((info) => { + const stack = new Error().stack?.split("\n"); + let file = "unknown"; + let line = 0; + + if (stack) { + for (let i = 2; i < stack.length; i++) { + const lineStr = stack[i].trim(); + if ( + !lineStr.includes("node_modules") && + !lineStr.includes(path.basename(import.meta.url)) + ) { + const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); + if (matches) { + file = path.basename(matches[1]); + line = Number.parseInt(matches[2], 10); + break; + } + } + } + } + return { ...info, file, line }; + })(), + format.printf((info) => { + const { timestamp, level, message, file, line } = + info as TransformableInfo & log_message; + let processedLevel = level as LogLevel; + let processedMessage = String(message); + + if (processedMessage.startsWith("__task__")) { + processedMessage = processedMessage + .replace(/__task__/g, "") + .trimStart(); + processedLevel = "task"; + if (processedMessage.startsWith("__db__")) { + processedMessage = processedMessage + .replace(/__db__/g, "") + .trimStart(); + processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; + } + } else if (processedMessage.startsWith("__UT__")) { + processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); + processedLevel = "ut"; + } + + if (file.endsWith("plugin.ts")) { + processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; + } + + const paddedLevel = processedLevel.toUpperCase().padEnd(5); + const coloredLevel = (levelColors[processedLevel] || chalk.white)( + paddedLevel + ); + const coloredContext = chalk.cyan(`${file}:${line}`); + const coloredTimestamp = chalk.yellow(timestamp); + + const prefix = `${paddedLevel} [ ${timestamp} ] - `; + const combinedContent = `${processedMessage} - ${coloredContext}`; + + const formattedMessage = padNewlines + ? formatTerminalMessage(combinedContent, prefix) + : combinedContent; + + handleDatabaseLog({ + level: processedLevel, + timestamp: timestamp, + message: processedMessage, + file: file, + line: line, + }); + handleWebSocketLog({ + level: processedLevel, + timestamp: timestamp, + message: processedMessage, + file: file, + line: line, + }); + + return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; + }) + ), + transports: [new transports.Console()], }); From 5b9f0547e0491cf113952633255cbfa88afd18b0 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 7 Jul 2025 11:25:27 +0000 Subject: [PATCH 353/369] CQL: Apply lint fixes [skip ci] --- src/core/utils/logger.ts | 328 +++++++++++++++++++-------------------- 1 file changed, 164 insertions(+), 164 deletions(-) diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index d3b891e1..483d73cf 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -16,188 +16,188 @@ import { backupInProgress } from "../database/_dbState"; const padNewlines = true; //process.env.PAD_NEW_LINES !== "false"; type LogLevel = - | "error" - | "warn" - | "info" - | "debug" - | "verbose" - | "silly" - | "task" - | "ut"; + | "error" + | "warn" + | "info" + | "debug" + | "verbose" + | "silly" + | "task" + | "ut"; // biome-ignore lint/suspicious/noControlCharactersInRegex: const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; const formatTerminalMessage = (message: string, prefix: string): string => { - try { - const cleanPrefix = prefix.replace(ansiRegex, ""); - const maxWidth = process.stdout.columns || 80; - const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); - - if (!padNewlines) return message; - - const wrapped = wrapAnsi(message, wrapWidth, { - trim: true, - hard: true, - wordWrap: true, - }); - - return wrapped - .split("\n") - .map((line, index) => { - return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; - }) - .join("\n"); - } catch (error) { - console.error("Error formatting terminal message:", error); - return message; - } + try { + const cleanPrefix = prefix.replace(ansiRegex, ""); + const maxWidth = process.stdout.columns || 80; + const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); + + if (!padNewlines) return message; + + const wrapped = wrapAnsi(message, wrapWidth, { + trim: true, + hard: true, + wordWrap: true, + }); + + return wrapped + .split("\n") + .map((line, index) => { + return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; + }) + .join("\n"); + } catch (error) { + console.error("Error formatting terminal message:", error); + return message; + } }; const levelColors: Record = { - error: chalk.red.bold, - warn: chalk.yellow.bold, - info: chalk.green.bold, - debug: chalk.blue.bold, - verbose: chalk.cyan.bold, - silly: chalk.magenta.bold, - task: chalk.cyan.bold, - ut: chalk.hex("#9D00FF"), + error: chalk.red.bold, + warn: chalk.yellow.bold, + info: chalk.green.bold, + debug: chalk.blue.bold, + verbose: chalk.cyan.bold, + silly: chalk.magenta.bold, + task: chalk.cyan.bold, + ut: chalk.hex("#9D00FF"), }; const parseTimestamp = (timestamp: string): string => { - const [datePart, timePart] = timestamp.split(" "); - const [day, month] = datePart.split("/"); - const [hours, minutes, seconds] = timePart.split(":"); - const year = new Date().getFullYear(); - const date = new Date( - year, - Number.parseInt(month) - 1, - Number.parseInt(day), - Number.parseInt(hours), - Number.parseInt(minutes), - Number.parseInt(seconds) - ); - return date.toISOString(); + const [datePart, timePart] = timestamp.split(" "); + const [day, month] = datePart.split("/"); + const [hours, minutes, seconds] = timePart.split(":"); + const year = new Date().getFullYear(); + const date = new Date( + year, + Number.parseInt(month) - 1, + Number.parseInt(day), + Number.parseInt(hours), + Number.parseInt(minutes), + Number.parseInt(seconds), + ); + return date.toISOString(); }; const handleWebSocketLog = (log: log_message) => { - try { - logToClients({ - ...log, - timestamp: parseTimestamp(log.timestamp), - }); - } catch (error) { - console.error( - `WebSocket logging failed: ${ - error instanceof Error ? error.message : error - }` - ); - } + try { + logToClients({ + ...log, + timestamp: parseTimestamp(log.timestamp), + }); + } catch (error) { + console.error( + `WebSocket logging failed: ${ + error instanceof Error ? error.message : error + }`, + ); + } }; const handleDatabaseLog = (log: log_message): void => { - if (backupInProgress) { - return; - } - try { - dbFunctions.addLogEntry({ - ...log, - timestamp: parseTimestamp(log.timestamp), - }); - } catch (error) { - console.error( - `Database logging failed: ${ - error instanceof Error ? error.message : error - }` - ); - } + if (backupInProgress) { + return; + } + try { + dbFunctions.addLogEntry({ + ...log, + timestamp: parseTimestamp(log.timestamp), + }); + } catch (error) { + console.error( + `Database logging failed: ${ + error instanceof Error ? error.message : error + }`, + ); + } }; export const logger = createLogger({ - level: process.env.LOG_LEVEL || "debug", - format: format.combine( - format.timestamp({ format: "DD/MM HH:mm:ss" }), - format((info) => { - const stack = new Error().stack?.split("\n"); - let file = "unknown"; - let line = 0; - - if (stack) { - for (let i = 2; i < stack.length; i++) { - const lineStr = stack[i].trim(); - if ( - !lineStr.includes("node_modules") && - !lineStr.includes(path.basename(import.meta.url)) - ) { - const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); - if (matches) { - file = path.basename(matches[1]); - line = Number.parseInt(matches[2], 10); - break; - } - } - } - } - return { ...info, file, line }; - })(), - format.printf((info) => { - const { timestamp, level, message, file, line } = - info as TransformableInfo & log_message; - let processedLevel = level as LogLevel; - let processedMessage = String(message); - - if (processedMessage.startsWith("__task__")) { - processedMessage = processedMessage - .replace(/__task__/g, "") - .trimStart(); - processedLevel = "task"; - if (processedMessage.startsWith("__db__")) { - processedMessage = processedMessage - .replace(/__db__/g, "") - .trimStart(); - processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; - } - } else if (processedMessage.startsWith("__UT__")) { - processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); - processedLevel = "ut"; - } - - if (file.endsWith("plugin.ts")) { - processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; - } - - const paddedLevel = processedLevel.toUpperCase().padEnd(5); - const coloredLevel = (levelColors[processedLevel] || chalk.white)( - paddedLevel - ); - const coloredContext = chalk.cyan(`${file}:${line}`); - const coloredTimestamp = chalk.yellow(timestamp); - - const prefix = `${paddedLevel} [ ${timestamp} ] - `; - const combinedContent = `${processedMessage} - ${coloredContext}`; - - const formattedMessage = padNewlines - ? formatTerminalMessage(combinedContent, prefix) - : combinedContent; - - handleDatabaseLog({ - level: processedLevel, - timestamp: timestamp, - message: processedMessage, - file: file, - line: line, - }); - handleWebSocketLog({ - level: processedLevel, - timestamp: timestamp, - message: processedMessage, - file: file, - line: line, - }); - - return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; - }) - ), - transports: [new transports.Console()], + level: process.env.LOG_LEVEL || "debug", + format: format.combine( + format.timestamp({ format: "DD/MM HH:mm:ss" }), + format((info) => { + const stack = new Error().stack?.split("\n"); + let file = "unknown"; + let line = 0; + + if (stack) { + for (let i = 2; i < stack.length; i++) { + const lineStr = stack[i].trim(); + if ( + !lineStr.includes("node_modules") && + !lineStr.includes(path.basename(import.meta.url)) + ) { + const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); + if (matches) { + file = path.basename(matches[1]); + line = Number.parseInt(matches[2], 10); + break; + } + } + } + } + return { ...info, file, line }; + })(), + format.printf((info) => { + const { timestamp, level, message, file, line } = + info as TransformableInfo & log_message; + let processedLevel = level as LogLevel; + let processedMessage = String(message); + + if (processedMessage.startsWith("__task__")) { + processedMessage = processedMessage + .replace(/__task__/g, "") + .trimStart(); + processedLevel = "task"; + if (processedMessage.startsWith("__db__")) { + processedMessage = processedMessage + .replace(/__db__/g, "") + .trimStart(); + processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; + } + } else if (processedMessage.startsWith("__UT__")) { + processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); + processedLevel = "ut"; + } + + if (file.endsWith("plugin.ts")) { + processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; + } + + const paddedLevel = processedLevel.toUpperCase().padEnd(5); + const coloredLevel = (levelColors[processedLevel] || chalk.white)( + paddedLevel, + ); + const coloredContext = chalk.cyan(`${file}:${line}`); + const coloredTimestamp = chalk.yellow(timestamp); + + const prefix = `${paddedLevel} [ ${timestamp} ] - `; + const combinedContent = `${processedMessage} - ${coloredContext}`; + + const formattedMessage = padNewlines + ? formatTerminalMessage(combinedContent, prefix) + : combinedContent; + + handleDatabaseLog({ + level: processedLevel, + timestamp: timestamp, + message: processedMessage, + file: file, + line: line, + }); + handleWebSocketLog({ + level: processedLevel, + timestamp: timestamp, + message: processedMessage, + file: file, + line: line, + }); + + return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; + }), + ), + transports: [new transports.Console()], }); From 083933096810901784bb2241c30a4bf9663d09bd Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 8 Jul 2025 22:34:00 +0200 Subject: [PATCH 354/369] feat(store): add store repos functionality This commit introduces the ability to manage store repositories within the application. It includes: - Creation of a table in the database to store repository information (slug and base URL). - Implementation of functions to add, retrieve, and delete store repositories from the database. - Creation of a new handler to expose store repository management functionalities via API. --- bun.lock | 515 ++++++++++++++++++++++++++++++++++ src/core/database/database.ts | 16 ++ src/core/database/index.ts | 2 + src/core/database/stores.ts | 31 ++ src/core/utils/logger.ts | 328 +++++++++++----------- src/handlers/index.ts | 7 +- src/handlers/logs.ts | 4 +- src/handlers/stacks.ts | 61 +++- src/handlers/store.ts | 51 ++++ typings | 2 +- 10 files changed, 844 insertions(+), 173 deletions(-) create mode 100644 bun.lock create mode 100644 src/core/database/stores.ts create mode 100644 src/handlers/store.ts diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..f9588ab6 --- /dev/null +++ b/bun.lock @@ -0,0 +1,515 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "dockstatapi", + "dependencies": { + "chalk": "^5.4.1", + "date-fns": "^4.1.0", + "docker-compose": "^1.2.0", + "dockerode": "^4.0.7", + "js-yaml": "^4.1.0", + "knip": "latest", + "split2": "^4.2.0", + "winston": "^3.17.0", + "yaml": "^2.8.0", + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@its_4_nik/gitai": "^1.1.14", + "@types/bun": "latest", + "@types/dockerode": "^3.3.42", + "@types/js-yaml": "^4.0.9", + "@types/node": "^22.16.0", + "@types/split2": "^4.2.3", + "bun-types": "latest", + "cross-env": "^7.0.3", + "logform": "^2.7.0", + "typescript": "^5.8.3", + "wrap-ansi": "^9.0.0", + }, + }, + }, + "trustedDependencies": [ + "protobufjs", + ], + "packages": { + "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], + + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + + "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], + + "@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" } }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], + + "@google/generative-ai": ["@google/generative-ai@0.24.1", "", {}, "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q=="], + + "@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="], + + "@inquirer/checkbox": ["@inquirer/checkbox@4.1.9", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/figures": "^1.0.12", "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-DBJBkzI5Wx4jFaYm221LHvAhpKYkhVS0k9plqHwaHhofGNxvYB7J3Bz8w+bFJ05zaMb0sZNHo4KdmENQFlNTuQ=="], + + "@inquirer/confirm": ["@inquirer/confirm@5.1.13", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/type": "^3.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-EkCtvp67ICIVVzjsquUiVSd+V5HRGOGQfsqA4E4vMWhYnB7InUL0pa0TIWt1i+OfP16Gkds8CdIu6yGZwOM1Yw=="], + + "@inquirer/core": ["@inquirer/core@10.1.14", "", { "dependencies": { "@inquirer/figures": "^1.0.12", "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Ma+ZpOJPewtIYl6HZHZckeX1STvDnHTCB2GVINNUlSEn2Am6LddWwfPkIGY0IUFVjUUrr/93XlBwTK6mfLjf0A=="], + + "@inquirer/editor": ["@inquirer/editor@4.2.14", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/type": "^3.0.7", "external-editor": "^3.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yd2qtLl4QIIax9DTMZ1ZN2pFrrj+yL3kgIWxm34SS6uwCr0sIhsNyudUjAo5q3TqI03xx4SEBkUJqZuAInp9uA=="], + + "@inquirer/expand": ["@inquirer/expand@4.0.16", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/type": "^3.0.7", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-oiDqafWzMtofeJyyGkb1CTPaxUkjIcSxePHHQCfif8t3HV9pHcw1Kgdw3/uGpDvaFfeTluwQtWiqzPVjAqS3zA=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.12", "", {}, "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ=="], + + "@inquirer/input": ["@inquirer/input@4.2.0", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/type": "^3.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-opqpHPB1NjAmDISi3uvZOTrjEEU5CWVu/HBkDby8t93+6UxYX0Z7Ps0Ltjm5sZiEbWenjubwUkivAEYQmy9xHw=="], + + "@inquirer/number": ["@inquirer/number@3.0.16", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/type": "^3.0.7" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kMrXAaKGavBEoBYUCgualbwA9jWUx2TjMA46ek+pEKy38+LFpL9QHlTd8PO2kWPUgI/KB+qi02o4y2rwXbzr3Q=="], + + "@inquirer/password": ["@inquirer/password@4.0.16", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-g8BVNBj5Zeb5/Y3cSN+hDUL7CsIFDIuVxb9EPty3lkxBaYpjL5BNRKSYOF9yOLe+JOcKFd+TSVeADQ4iSY7rbg=="], + + "@inquirer/prompts": ["@inquirer/prompts@7.6.0", "", { "dependencies": { "@inquirer/checkbox": "^4.1.9", "@inquirer/confirm": "^5.1.13", "@inquirer/editor": "^4.2.14", "@inquirer/expand": "^4.0.16", "@inquirer/input": "^4.2.0", "@inquirer/number": "^3.0.16", "@inquirer/password": "^4.0.16", "@inquirer/rawlist": "^4.1.4", "@inquirer/search": "^3.0.16", "@inquirer/select": "^4.2.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-jAhL7tyMxB3Gfwn4HIJ0yuJ5pvcB5maYUcouGcgd/ub79f9MqZ+aVnBtuFf+VC2GTkCBF+R+eo7Vi63w5VZlzw=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@4.1.4", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/type": "^3.0.7", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5GGvxVpXXMmfZNtvWw4IsHpR7RzqAR624xtkPd1NxxlV5M+pShMqzL4oRddRkg8rVEOK9fKdJp1jjVML2Lr7TQ=="], + + "@inquirer/search": ["@inquirer/search@3.0.16", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/figures": "^1.0.12", "@inquirer/type": "^3.0.7", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-POCmXo+j97kTGU6aeRjsPyuCpQQfKcMXdeTMw708ZMtWrj5aykZvlUxH4Qgz3+Y1L/cAVZsSpA+UgZCu2GMOMg=="], + + "@inquirer/select": ["@inquirer/select@4.2.4", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/figures": "^1.0.12", "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-unTppUcTjmnbl/q+h8XeQDhAqIOmwWYWNyiiP2e3orXrg6tOaa5DHXja9PChCSbChOsktyKgOieRZFnajzxoBg=="], + + "@inquirer/type": ["@inquirer/type@3.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA=="], + + "@its_4_nik/gitai": ["@its_4_nik/gitai@1.1.14", "", { "dependencies": { "@google/generative-ai": "^0.24.1", "commander": "^14.0.0", "ignore": "^7.0.5", "inquirer": "^12.6.3", "ollama": "^0.5.16" }, "peerDependencies": { "typescript": "^5.8.3" }, "bin": { "gitai": "dist/gitai.js" } }, "sha512-vpZnCWtgMcfqPNpkjOpEG3+dEr+t87C0wlH+FOiHDiLVw2ebZir9QJiw7yOl75hhkxHqXVDnluj6U0e3yAfzqA=="], + + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" } }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.5.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IQZZP6xjGvVNbXVPEwZeCDTkG7iajFsVZSaq7QwxuiJqkcE/GKd0GxGQMs6jjE72nrgSGVHQD/yws1PNzP9j5w=="], + + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.5.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-nY15IBY5NjOPKIDRJ2sSLr0GThFXz4J4lgIo4fmnXanJjeeXaM5aCOL3oIxT7RbONqyMki0lzMkbX7PWqW3/lw=="], + + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.5.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WQibNtsWiJZ36Q2QKYSedN6c4xoZtLhU7UOFPGTMaw/J8eb+WYh5pfzTtZR9WGZQRoS3kj0E/9683Wuskz5mMQ=="], + + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.5.0", "", { "os": "linux", "cpu": "arm" }, "sha512-oZj20OTnjGn1qnBGYTjRXEMyd0inlw127s+DTC+Y0kdxoz5BUMqUhq5M9mZ1BH4c1qPlRto6shOFVrK4hNkhhA=="], + + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.5.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zxFuO4Btd1BSFjuaO0mnIA9XRWP4FX3bTbVO9KjKvO8MX6Ig2+ZDNHpzzK2zkOunHGc4sJQm5oDTcMvww+hyag=="], + + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.5.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-mmDNrt2yyEnsPrmq3wzRsqEYM+cpVuv8itgYU++BNJrfzdJpK+OpvR3rPToTZSOZQt3iYLfqQ2hauIIraJnJGw=="], + + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.5.0", "", { "os": "linux", "cpu": "none" }, "sha512-CxW3/uVUlSpIEJ3sLi5Q+lk7SVgQoxUKBTsMwpY2nFiCmtzHBOuwMMKES1Hk+w/Eirz09gDjoIrxkzg3ETDSGQ=="], + + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.5.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-RxfVqJnmO7uEGzpEgEzVb5Sxjy8NAYpQj+7JZZunxIyJiDK1KgOJqVJ0NZnRC1UAe/yyEpO82wQIOInaLqFBgA=="], + + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.5.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Ri36HuV91PVXFw1BpTisJOZ2x9dkfgsvrjVa3lPX+QS6QRvvcdogGjPTTqgg8WkzCh6RTzd7Lx9mCZQdw06HTQ=="], + + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.5.0", "", { "os": "linux", "cpu": "x64" }, "sha512-xskd2J4Jnfuze2jYKiZx4J+PY4hJ5Z0MuVh8JPNvu/FY1+SAdRei9S95dhc399Nw6eINre7xOrsugr11td3k4Q=="], + + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.5.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-ZAHTs0MzHUlHAqKffvutprVhO7OlENWisu1fW/bVY6r+TPxsl25Q0lzbOUhrxTIJ9f0Sl5meCI2fkPeovZA7bQ=="], + + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.5.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-4/3RJnkrKo7EbBdWAYsSHZEjgZ8TYYAt/HrHDo5yy/5dUvxvPoetNtAudCiYKNgJOlFLzmzIXyn713MljEy6RA=="], + + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.5.0", "", { "os": "win32", "cpu": "x64" }, "sha512-poXrxQLJA770Xy3gAS9mrC/dp6GatYdvNlwCWwjL6lzBNToEK66kx3tgqIaOYIqtjJDKYR58P3jWgmwJyJxEAQ=="], + + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.6.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-m3wyqBh1TOHjpr/dXeIZY7OoX+MQazb+bMHQdDtwUvefrafUx+5YHRvulYh1sZSQ449nQ3nk3qj5qj535vZRjg=="], + + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.6.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-75fJfF/9xNypr7cnOYoZBhfmG1yP7ex3pUOeYGakmtZRffO9z1i1quLYhjZsmaDXsAIZ3drMhenYHMmFKS3SRg=="], + + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-YhXGf0FXa72bEt4F7eTVKx5X3zWpbAOPnaA/dZ6/g8tGhw1m9IFjrabVHFjzcx3dQny4MgA59EhyElkDvpUe8A=="], + + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-T3JDhx8mjGjvh5INsPZJrlKHmZsecgDYvtvussKRdkc1Nnn7WC+jH9sh5qlmYvwzvmetlPVNezAoNvmGO9vtMg=="], + + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dx7ghtAl8aXBdqofJpi338At6lkeCtTfoinTYQXd9/TEJx+f+zCGNlQO6nJz3ydJBX48FDuOFKkNC+lUlWrd8w=="], + + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-7KvMGdWmAZtAtg6IjoEJHKxTXdAcrHnUnqfgs0JpXst7trquV2mxBeRZusQXwxpu4HCSomKMvJfsp1qKaqSFDg=="], + + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.6.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-iSGC9RwX+dl7o5KFr5aH7Gq3nFbkq/3Gda6mxNPMvNkWrgXdIyiINxpyD8hJu566M+QSv1wEAu934BZotFDyoQ=="], + + "@oxlint/win32-x64": ["@oxlint/win32-x64@1.6.0", "", { "os": "win32", "cpu": "x64" }, "sha512-jOj3L/gfLc0IwgOTkZMiZ5c673i/hbAmidlaylT0gE6H18hln9HxPgp5GCf4E4y6mwEJlW8QC5hQi221+9otdA=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], + + "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + + "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], + + "@types/dockerode": ["@types/dockerode@3.3.42", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-U1jqHMShibMEWHdxYhj3rCMNCiLx5f35i4e3CEUuW+JSSszc/tVqc6WCAPdhwBymG5R/vgbcceagK0St7Cq6Eg=="], + + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + + "@types/node": ["@types/node@22.16.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ=="], + + "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + + "@types/split2": ["@types/split2@4.2.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw=="], + + "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], + + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], + + "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], + + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], + + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + + "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], + + "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], + + "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "docker-compose": ["docker-compose@1.2.0", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-wIU1eHk3Op7dFgELRdmOYlPYS4gP8HhH1ZmZa13QZF59y0fblzFDFmKPhyc05phCy2hze9OEvNZAsoljrs+72w=="], + + "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], + + "dockerode": ["dockerode@4.0.7", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.1.2", "uuid": "^10.0.0" } }, "sha512-R+rgrSRTRdU5mH14PZTCPZtW/zw3HDWNTS/1ZAQpL/5Upe/ye5K9WQkIysu4wBoiMwKynsz0a8qWuGsHgEvSAA=="], + + "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="], + + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + + "formatly": ["formatly@0.2.4", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "inquirer": ["inquirer@12.7.0", "", { "dependencies": { "@inquirer/core": "^10.1.14", "@inquirer/prompts": "^7.6.0", "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "mute-stream": "^2.0.0", "run-async": "^4.0.4", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KKFRc++IONSyE2UYw9CJ1V0IWx5yQKomwB+pp3cWomWs+v2+ZsG11G2OVfAjFS6WWCppKw+RfKmpqGfSzD5QBQ=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "knip": ["knip@5.61.3", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.2.4", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "oxc-resolver": "^11.1.0", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.3.4", "strip-json-comments": "5.0.2", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-8iSz8i8ufIjuUwUKzEwye7ROAW0RzCze7T770bUiz0PKL+SSwbs4RS32fjMztLwcOzSsNPlXdUAeqmkdzXxJ1Q=="], + + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], + + "nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="], + + "ollama": ["ollama@0.5.16", "", { "dependencies": { "whatwg-fetch": "^3.6.20" } }, "sha512-OEbxxOIUZtdZgOaTPAULo051F5y+Z1vosxEYOoABPnQKeW7i4O8tJNlxCB+xioyoorVqgjkdj+TA1f1Hy2ug/w=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + + "oxc-resolver": ["oxc-resolver@11.5.0", "", { "optionalDependencies": { "@oxc-resolver/binding-darwin-arm64": "11.5.0", "@oxc-resolver/binding-darwin-x64": "11.5.0", "@oxc-resolver/binding-freebsd-x64": "11.5.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.5.0", "@oxc-resolver/binding-linux-arm64-gnu": "11.5.0", "@oxc-resolver/binding-linux-arm64-musl": "11.5.0", "@oxc-resolver/binding-linux-riscv64-gnu": "11.5.0", "@oxc-resolver/binding-linux-s390x-gnu": "11.5.0", "@oxc-resolver/binding-linux-x64-gnu": "11.5.0", "@oxc-resolver/binding-linux-x64-musl": "11.5.0", "@oxc-resolver/binding-wasm32-wasi": "11.5.0", "@oxc-resolver/binding-win32-arm64-msvc": "11.5.0", "@oxc-resolver/binding-win32-x64-msvc": "11.5.0" } }, "sha512-lG/AiquYQP/4OOXaKmlPvLeCOxtlZ535489H3yk4euimwnJXIViQus2Y9Mc4c45wFQ0UYM1rFduiJ8+RGjUtTQ=="], + + "oxlint": ["oxlint@1.6.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.6.0", "@oxlint/darwin-x64": "1.6.0", "@oxlint/linux-arm64-gnu": "1.6.0", "@oxlint/linux-arm64-musl": "1.6.0", "@oxlint/linux-x64-gnu": "1.6.0", "@oxlint/linux-x64-musl": "1.6.0", "@oxlint/win32-arm64": "1.6.0", "@oxlint/win32-x64": "1.6.0" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-jtaD65PqzIa1udvSxxscTKBxYKuZoFXyKGLiU1Qjo1ulq3uv/fQDtoV1yey1FrQZrQjACGPi1Widsy1TucC7Jg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + + "protobufjs": ["protobufjs@7.5.3", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw=="], + + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "run-async": ["run-async@4.0.4", "", { "dependencies": { "oxlint": "^1.2.0", "prettier": "^3.5.3" } }, "sha512-2cgeRHnV11lSXBEhq7sN7a5UVjTKm9JTb9x8ApIT//16D7QL96AgnNeWSGoB4gIHc0iYw/Ha0Z+waBaCYZVNhg=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "smol-toml": ["smol-toml@1.4.1", "", {}, "sha512-CxdwHXyYTONGHThDbq5XdwbFsuY4wlClRGejfE2NtwUtiHYsP1QtNsHb/hnj31jKYSchztJsaA8pSQoVzkfCFg=="], + + "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="], + + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "strip-json-comments": ["strip-json-comments@5.0.2", "", {}, "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g=="], + + "tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + + "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + + "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], + + "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], + + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + + "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="], + + "zod": ["zod@3.25.75", "", {}, "sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg=="], + + "zod-validation-error": ["zod-validation-error@3.5.2", "", { "peerDependencies": { "zod": "^3.25.0" } }, "sha512-mdi7YOLtram5dzJ5aDtm1AG9+mxRma1iaMrZdYIpFO7epdKBUwLHIxTF8CPDeCQ828zAXYtizrKlEJAtzgfgrw=="], + + "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "@types/ssh2/@types/node": ["@types/node@18.19.115", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-kNrFiTgG4a9JAn1LMQeLOv3MvXIPokzXziohMrMsvpYgLpdEt/mMiVYc4sGKtDfyxM5gIDF4VgrPRyCw4fHOYg=="], + + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@inquirer/core/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@inquirer/core/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + } +} diff --git a/src/core/database/database.ts b/src/core/database/database.ts index 3a9311c3..c5ee7c4f 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -91,6 +91,11 @@ export function init() { CREATE TABLE IF NOT EXISTS config ( keep_data_for NUMBER NOT NULL, fetching_interval NUMBER NOT NULL ); + + CREATE TABLE IF NOT EXISTS store_repos ( + slug TEXT NOT NULL, + base TEXT NOT NULL + ); `); const configRow = db @@ -112,6 +117,17 @@ export function init() { "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", ).run("Localhost", "localhost:2375", false); } + + const storeRow = db + .prepare("SELECT COUNT(*) AS count FROM store_repos") + .get() as { count: number }; + + if (storeRow.count === 0) { + db.prepare("INSERT INTO store_repos (slug, base) VALUES (?, ?)").run( + "DockStacks", + "https://raw.githubusercontent.com/Its4Nik/DockStacks/refs/heads/main/Index.json", + ); + } } init(); diff --git a/src/core/database/index.ts b/src/core/database/index.ts index c381e7a6..8e2a9f0f 100644 --- a/src/core/database/index.ts +++ b/src/core/database/index.ts @@ -9,6 +9,7 @@ import * as dockerHosts from "~/core/database/dockerHosts"; import * as hostStats from "~/core/database/hostStats"; import * as logs from "~/core/database/logs"; import * as stacks from "~/core/database/stacks"; +import * as stores from "~/core/database/stores"; export const dbFunctions = { ...dockerHosts, @@ -18,6 +19,7 @@ export const dbFunctions = { ...hostStats, ...stacks, ...backup, + ...stores, }; export type dbFunctions = typeof dbFunctions; diff --git a/src/core/database/stores.ts b/src/core/database/stores.ts new file mode 100644 index 00000000..c8a330cb --- /dev/null +++ b/src/core/database/stores.ts @@ -0,0 +1,31 @@ +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const stmt = { + insert: db.prepare(` + INSERT INTO store_repos (slug, base) VALUES (?, ?) + `), + selectAll: db.prepare(` + SELECT slug, base FROM store_repos + `), + delete: db.prepare(` + DELETE FROM store_repos WHERE slug = ? + `), +}; + +export function getStoreRepos() { + return executeDbOperation("Get Store Repos", () => stmt.selectAll.all()) as { + slug: string; + base: string; + }[]; +} + +export function addStoreRepo(slug: string, base: string) { + return executeDbOperation("Add Store Repo", () => + stmt.insert.run(slug, base), + ); +} + +export function deleteStoreRepo(slug: string) { + return executeDbOperation("Delete Store Repo", () => stmt.delete.run(slug)); +} diff --git a/src/core/utils/logger.ts b/src/core/utils/logger.ts index d3b891e1..483d73cf 100644 --- a/src/core/utils/logger.ts +++ b/src/core/utils/logger.ts @@ -16,188 +16,188 @@ import { backupInProgress } from "../database/_dbState"; const padNewlines = true; //process.env.PAD_NEW_LINES !== "false"; type LogLevel = - | "error" - | "warn" - | "info" - | "debug" - | "verbose" - | "silly" - | "task" - | "ut"; + | "error" + | "warn" + | "info" + | "debug" + | "verbose" + | "silly" + | "task" + | "ut"; // biome-ignore lint/suspicious/noControlCharactersInRegex: const ansiRegex = /\x1B\[[0-?9;]*[mG]/g; const formatTerminalMessage = (message: string, prefix: string): string => { - try { - const cleanPrefix = prefix.replace(ansiRegex, ""); - const maxWidth = process.stdout.columns || 80; - const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); - - if (!padNewlines) return message; - - const wrapped = wrapAnsi(message, wrapWidth, { - trim: true, - hard: true, - wordWrap: true, - }); - - return wrapped - .split("\n") - .map((line, index) => { - return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; - }) - .join("\n"); - } catch (error) { - console.error("Error formatting terminal message:", error); - return message; - } + try { + const cleanPrefix = prefix.replace(ansiRegex, ""); + const maxWidth = process.stdout.columns || 80; + const wrapWidth = Math.max(maxWidth - cleanPrefix.length - 3, 20); + + if (!padNewlines) return message; + + const wrapped = wrapAnsi(message, wrapWidth, { + trim: true, + hard: true, + wordWrap: true, + }); + + return wrapped + .split("\n") + .map((line, index) => { + return index === 0 ? line : `${" ".repeat(cleanPrefix.length)}${line}`; + }) + .join("\n"); + } catch (error) { + console.error("Error formatting terminal message:", error); + return message; + } }; const levelColors: Record = { - error: chalk.red.bold, - warn: chalk.yellow.bold, - info: chalk.green.bold, - debug: chalk.blue.bold, - verbose: chalk.cyan.bold, - silly: chalk.magenta.bold, - task: chalk.cyan.bold, - ut: chalk.hex("#9D00FF"), + error: chalk.red.bold, + warn: chalk.yellow.bold, + info: chalk.green.bold, + debug: chalk.blue.bold, + verbose: chalk.cyan.bold, + silly: chalk.magenta.bold, + task: chalk.cyan.bold, + ut: chalk.hex("#9D00FF"), }; const parseTimestamp = (timestamp: string): string => { - const [datePart, timePart] = timestamp.split(" "); - const [day, month] = datePart.split("/"); - const [hours, minutes, seconds] = timePart.split(":"); - const year = new Date().getFullYear(); - const date = new Date( - year, - Number.parseInt(month) - 1, - Number.parseInt(day), - Number.parseInt(hours), - Number.parseInt(minutes), - Number.parseInt(seconds) - ); - return date.toISOString(); + const [datePart, timePart] = timestamp.split(" "); + const [day, month] = datePart.split("/"); + const [hours, minutes, seconds] = timePart.split(":"); + const year = new Date().getFullYear(); + const date = new Date( + year, + Number.parseInt(month) - 1, + Number.parseInt(day), + Number.parseInt(hours), + Number.parseInt(minutes), + Number.parseInt(seconds), + ); + return date.toISOString(); }; const handleWebSocketLog = (log: log_message) => { - try { - logToClients({ - ...log, - timestamp: parseTimestamp(log.timestamp), - }); - } catch (error) { - console.error( - `WebSocket logging failed: ${ - error instanceof Error ? error.message : error - }` - ); - } + try { + logToClients({ + ...log, + timestamp: parseTimestamp(log.timestamp), + }); + } catch (error) { + console.error( + `WebSocket logging failed: ${ + error instanceof Error ? error.message : error + }`, + ); + } }; const handleDatabaseLog = (log: log_message): void => { - if (backupInProgress) { - return; - } - try { - dbFunctions.addLogEntry({ - ...log, - timestamp: parseTimestamp(log.timestamp), - }); - } catch (error) { - console.error( - `Database logging failed: ${ - error instanceof Error ? error.message : error - }` - ); - } + if (backupInProgress) { + return; + } + try { + dbFunctions.addLogEntry({ + ...log, + timestamp: parseTimestamp(log.timestamp), + }); + } catch (error) { + console.error( + `Database logging failed: ${ + error instanceof Error ? error.message : error + }`, + ); + } }; export const logger = createLogger({ - level: process.env.LOG_LEVEL || "debug", - format: format.combine( - format.timestamp({ format: "DD/MM HH:mm:ss" }), - format((info) => { - const stack = new Error().stack?.split("\n"); - let file = "unknown"; - let line = 0; - - if (stack) { - for (let i = 2; i < stack.length; i++) { - const lineStr = stack[i].trim(); - if ( - !lineStr.includes("node_modules") && - !lineStr.includes(path.basename(import.meta.url)) - ) { - const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); - if (matches) { - file = path.basename(matches[1]); - line = Number.parseInt(matches[2], 10); - break; - } - } - } - } - return { ...info, file, line }; - })(), - format.printf((info) => { - const { timestamp, level, message, file, line } = - info as TransformableInfo & log_message; - let processedLevel = level as LogLevel; - let processedMessage = String(message); - - if (processedMessage.startsWith("__task__")) { - processedMessage = processedMessage - .replace(/__task__/g, "") - .trimStart(); - processedLevel = "task"; - if (processedMessage.startsWith("__db__")) { - processedMessage = processedMessage - .replace(/__db__/g, "") - .trimStart(); - processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; - } - } else if (processedMessage.startsWith("__UT__")) { - processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); - processedLevel = "ut"; - } - - if (file.endsWith("plugin.ts")) { - processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; - } - - const paddedLevel = processedLevel.toUpperCase().padEnd(5); - const coloredLevel = (levelColors[processedLevel] || chalk.white)( - paddedLevel - ); - const coloredContext = chalk.cyan(`${file}:${line}`); - const coloredTimestamp = chalk.yellow(timestamp); - - const prefix = `${paddedLevel} [ ${timestamp} ] - `; - const combinedContent = `${processedMessage} - ${coloredContext}`; - - const formattedMessage = padNewlines - ? formatTerminalMessage(combinedContent, prefix) - : combinedContent; - - handleDatabaseLog({ - level: processedLevel, - timestamp: timestamp, - message: processedMessage, - file: file, - line: line, - }); - handleWebSocketLog({ - level: processedLevel, - timestamp: timestamp, - message: processedMessage, - file: file, - line: line, - }); - - return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; - }) - ), - transports: [new transports.Console()], + level: process.env.LOG_LEVEL || "debug", + format: format.combine( + format.timestamp({ format: "DD/MM HH:mm:ss" }), + format((info) => { + const stack = new Error().stack?.split("\n"); + let file = "unknown"; + let line = 0; + + if (stack) { + for (let i = 2; i < stack.length; i++) { + const lineStr = stack[i].trim(); + if ( + !lineStr.includes("node_modules") && + !lineStr.includes(path.basename(import.meta.url)) + ) { + const matches = lineStr.match(/\(?(.+):(\d+):(\d+)\)?$/); + if (matches) { + file = path.basename(matches[1]); + line = Number.parseInt(matches[2], 10); + break; + } + } + } + } + return { ...info, file, line }; + })(), + format.printf((info) => { + const { timestamp, level, message, file, line } = + info as TransformableInfo & log_message; + let processedLevel = level as LogLevel; + let processedMessage = String(message); + + if (processedMessage.startsWith("__task__")) { + processedMessage = processedMessage + .replace(/__task__/g, "") + .trimStart(); + processedLevel = "task"; + if (processedMessage.startsWith("__db__")) { + processedMessage = processedMessage + .replace(/__db__/g, "") + .trimStart(); + processedMessage = `${chalk.magenta("DB")} ${processedMessage}`; + } + } else if (processedMessage.startsWith("__UT__")) { + processedMessage = processedMessage.replace(/__UT__/g, "").trimStart(); + processedLevel = "ut"; + } + + if (file.endsWith("plugin.ts")) { + processedMessage = `[ ${chalk.grey(file)} ] ${processedMessage}`; + } + + const paddedLevel = processedLevel.toUpperCase().padEnd(5); + const coloredLevel = (levelColors[processedLevel] || chalk.white)( + paddedLevel, + ); + const coloredContext = chalk.cyan(`${file}:${line}`); + const coloredTimestamp = chalk.yellow(timestamp); + + const prefix = `${paddedLevel} [ ${timestamp} ] - `; + const combinedContent = `${processedMessage} - ${coloredContext}`; + + const formattedMessage = padNewlines + ? formatTerminalMessage(combinedContent, prefix) + : combinedContent; + + handleDatabaseLog({ + level: processedLevel, + timestamp: timestamp, + message: processedMessage, + file: file, + line: line, + }); + handleWebSocketLog({ + level: processedLevel, + timestamp: timestamp, + message: processedMessage, + file: file, + line: line, + }); + + return `${coloredLevel} [ ${coloredTimestamp} ] - ${formattedMessage}`; + }), + ), + transports: [new transports.Console()], }); diff --git a/src/handlers/index.ts b/src/handlers/index.ts index c8d8e7bb..7999857d 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -1,14 +1,10 @@ -import { setSchedules } from "~/core/docker/scheduler"; -import { pluginManager } from "~/core/plugins/plugin-manager"; -import { logger } from "~/core/utils/logger"; import { ApiHandler } from "./config"; import { DatabaseHandler } from "./database"; import { BasicDockerHandler } from "./docker"; import { LogHandler } from "./logs"; -import { startDockerStatsBroadcast } from "./modules/docker-socket"; import { Starter } from "./modules/starter"; -import { Sockets } from "./sockets"; import { StackHandler } from "./stacks"; +import { StoreHandler } from "./store"; import { CheckHealth } from "./utils"; export const handlers = { @@ -19,6 +15,7 @@ export const handlers = { LogHandler, CheckHealth, Socket: "ws://localhost:4837/ws", + StoreHandler, }; Starter.startAll(); diff --git a/src/handlers/logs.ts b/src/handlers/logs.ts index 5ab74593..766e60d9 100644 --- a/src/handlers/logs.ts +++ b/src/handlers/logs.ts @@ -19,8 +19,8 @@ class logHandler { logger.debug(`Retrieved logs (level: ${level})`); return logs; } catch (error) { - logger.error("Failed to retrieve logs"); - throw new Error("Failed to retrieve logs"); + logger.error(`Failed to retrieve logs: ${error}`); + throw new Error(`Failed to retrieve logs: ${error}`); } } diff --git a/src/handlers/stacks.ts b/src/handlers/stacks.ts index 34029875..cabf5836 100644 --- a/src/handlers/stacks.ts +++ b/src/handlers/stacks.ts @@ -13,6 +13,23 @@ import { logger } from "~/core/utils/logger"; import type { stacks_config } from "~/typings/database"; class stackHandler { + /** + * Deploys a Stack on the DockStatAPI + * + * @example + * ```ts + * deploy({ + * id: 0, + * name: "example", + * vesion: 1, + * custom: false, + * source: "https://github.com/Its4Nik/DockStacks" + * compose_spec: "{services: {web: {image: "nginx:latest",ports: ["80:80"]}}" + * }) + * ``` + * @param config + * @returns "Stack ${config.name} deployed successfully" + */ async deploy(config: stacks_config) { try { await deployStack(config); @@ -24,7 +41,11 @@ class stackHandler { return `${errorMsg}, Error deploying stack, please check the server logs for more information`; } } - + /** + * Runs `docker compose -f "./stacks/[StackID]-[StackName]" up -d` + * @param stackId + * @returns `Started Stack (${stackId})` + */ async start(stackId: number) { try { if (!stackId) { @@ -40,6 +61,11 @@ class stackHandler { } } + /** + * Runs `docker compose -f "./stacks/[StackID]-[StackName]" down` + * @param stackId + * @returns `Stack ${stackId} stopped successfully` + */ async stop(stackId: number) { try { if (!stackId) { @@ -55,6 +81,11 @@ class stackHandler { } } + /** + * Runs `docker compose -f "./stacks/[StackID]-[StackName]" restart` + * @param stackId + * @returns `Stack ${stackId} restarted successfully` + */ async restart(stackId: number) { try { if (!stackId) { @@ -70,6 +101,11 @@ class stackHandler { } } + /** + * Runs `docker compose -f "./stacks/[StackID]-[StackName]" pull` + * @param stackId + * @returns `Images for stack ${stackId} pulled successfully` + */ async pullImages(stackId: number) { try { if (!stackId) { @@ -85,6 +121,11 @@ class stackHandler { } } + /** + * Runs `docker compose -f "./stacks/[StackID]-[StackName]" ps` with custom formatting + * @param stackId + * @returns Idfk + */ async getStatus(stackId?: number) { if (stackId) { const status = await getStackStatus(stackId); @@ -101,6 +142,19 @@ class stackHandler { return status; } + /** + * @example + * ```json + * [{ + * id: 1; + * name: "example"; + * version: 1; + * custom: false; + * source: "https://github.com/Its4Nik/DockStacks"; + * compose_spec: "{services: {web: {image: "nginx:latest",ports: ["80:80"]}}" + * }] + * ``` + */ listStacks(): stacks_config[] { try { const stacks = dbFunctions.getStacks(); @@ -112,6 +166,11 @@ class stackHandler { } } + /** + * Deletes a whole Stack and it's local folder, this action is irreversible + * @param stackId + * @returns `Stack ${stackId} deleted successfully` + */ async deleteStack(stackId: number) { try { await removeStack(stackId); diff --git a/src/handlers/store.ts b/src/handlers/store.ts new file mode 100644 index 00000000..4cb83c45 --- /dev/null +++ b/src/handlers/store.ts @@ -0,0 +1,51 @@ +import { + addStoreRepo, + deleteStoreRepo, + getStoreRepos, +} from "~/core/database/stores"; + +class store { + /** + * + * @returns an Array of all Repos added to the Database + * @example + * ```json + * [ + * { + * slug: "DockStacks", + * base: "https://raw.githubusercontent.com/Its4Nik/DockStacks/refs/heads/main/Index.json" + * } + * ] + * ``` + */ + getRepos(): { + slug: string; + base: string; + }[] { + return getStoreRepos(); + } + + /** + * + * @param slug - "Nickname" for this repo + * @param base - The raw URL of where the [ROOT].json is located + * @example + * ```ts + * addRepo("DockStacks", "https://raw.githubusercontent.com/Its4Nik/DockStacks/refs/heads/main/Index.json") + * ``` + */ + addRepo(slug: string, base: string) { + return addStoreRepo(slug, base); + } + + /** + * Deletes a Repo from the Database + * @param slug + * @returns Changes + */ + deleteRepo(slug: string) { + return deleteStoreRepo(slug); + } +} + +export const StoreHandler = new store(); diff --git a/typings b/typings index ca039be0..01123928 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit ca039be0adea6274850c016c64595ee907a8ba3f +Subproject commit 01123928a672ac823b8371114fae75beca3f2442 From a710987c1559ccd208219c14fbd50ee799fe3b50 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Tue, 8 Jul 2025 20:36:25 +0000 Subject: [PATCH 355/369] Update dependency graphs --- dependency-graph.mmd | 217 ++++--- dependency-graph.svg | 1392 +++++++++++++++++++++--------------------- 2 files changed, 788 insertions(+), 821 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 6e348fe4..95a9101d 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -13,15 +13,14 @@ subgraph 2["handlers"] 4["config.ts"] subgraph P["modules"] Q["logs-socket.ts"] -1C["docker-socket.ts"] -1F["starter.ts"] -1K["live-stacks.ts"] +1D["starter.ts"] +1E["docker-socket.ts"] end -16["database.ts"] -17["docker.ts"] -1B["logs.ts"] -1J["sockets.ts"] -1L["stacks.ts"] +17["database.ts"] +18["docker.ts"] +1C["logs.ts"] +1K["stacks.ts"] +1T["store.ts"] 1U["utils.ts"] end subgraph B["core"] @@ -37,31 +36,32 @@ U["dockerHosts.ts"] V["hostStats.ts"] W["logs.ts"] X["stacks.ts"] +Z["stores.ts"] end subgraph N["utils"] O["logger.ts"] Y["helpers.ts"] -13["change-me-checker.ts"] -14["package-json.ts"] -1E["calculations.ts"] +14["change-me-checker.ts"] +15["package-json.ts"] +1G["calculations.ts"] end -subgraph Z["plugins"] -10["plugin-manager.ts"] -12["loader.ts"] +subgraph 10["plugins"] +11["plugin-manager.ts"] +13["loader.ts"] end -subgraph 19["docker"] -1A["client.ts"] -1G["scheduler.ts"] -1H["store-container-stats.ts"] -1I["store-host-stats.ts"] +subgraph 1A["docker"] +1B["client.ts"] +1H["scheduler.ts"] +1I["store-container-stats.ts"] +1J["store-host-stats.ts"] end -subgraph 1M["stacks"] -1N["controller.ts"] -1P["checker.ts"] -subgraph 1Q["operations"] -1R["runStackCommand.ts"] -1S["stackHelpers.ts"] -1T["stackStatus.ts"] +subgraph 1L["stacks"] +1M["controller.ts"] +1O["checker.ts"] +subgraph 1P["operations"] +1Q["runStackCommand.ts"] +1R["stackHelpers.ts"] +1S["stackStatus.ts"] end end end @@ -72,9 +72,9 @@ subgraph 6["typings"] 8["docker"] 9["plugin"] F["misc"] -18["dockerode"] -1D["websocket"] -1O["docker-compose"] +19["dockerode"] +1F["websocket"] +1N["docker-compose"] end end subgraph A["fs"] @@ -84,26 +84,22 @@ I["bun:sqlite"] K["os"] L["path"] R["stream"] -11["events"] -15["package.json"] +12["events"] +16["package.json"] 1-->3 3-->4 -3-->16 3-->17 -3-->1B +3-->18 3-->1C -3-->1F -3-->1J -3-->1L +3-->1D +3-->1K +3-->1T 3-->1U -3-->1G -3-->10 -3-->O 4-->D 4-->E -4-->10 +4-->11 4-->O -4-->14 +4-->15 4-->7 4-->8 4-->9 @@ -116,6 +112,7 @@ D-->U D-->V D-->W D-->X +D-->Z E-->G E-->H E-->M @@ -129,9 +126,9 @@ H-->K H-->L M-->G M-->O +O-->Q O-->G O-->D -O-->Q O-->7 O-->L Q-->O @@ -156,86 +153,84 @@ X-->H X-->M X-->7 Y-->O -10-->O -10-->12 -10-->8 -10-->9 -10-->11 -12-->13 -12-->O -12-->10 -12-->A -12-->L +Z-->H +Z-->M +11-->O +11-->13 +11-->8 +11-->9 +11-->12 +13-->14 13-->O -13-->J -14-->15 -16-->D +13-->11 +13-->A +13-->L +14-->O +14-->J +15-->16 17-->D -17-->1A -17-->O -17-->8 -17-->18 -1A-->O -1A-->8 -1B-->D +18-->D +18-->1B +18-->O +18-->8 +18-->19 1B-->O -1C-->Q +1B-->8 1C-->D -1C-->1A -1C-->1E 1C-->O -1C-->7 -1C-->8 -1C-->1D -1F-->1C -1F-->1G -1F-->10 -1G-->D -1G-->1H -1G-->1I -1G-->O -1G-->7 -1H-->O +1D-->1E +1D-->1H +1D-->11 +1E-->Q +1E-->D +1E-->1B +1E-->1G +1E-->O +1E-->7 +1E-->8 +1E-->1F 1H-->D -1H-->1A -1H-->1E +1H-->1I +1H-->1J +1H-->O 1H-->7 -1I-->D -1I-->1A 1I-->O -1I-->8 -1I-->18 -1J-->1C -1J-->1K -1J-->Q +1I-->D +1I-->1B +1I-->1G +1I-->7 +1J-->D +1J-->1B +1J-->O +1J-->8 +1J-->19 +1K-->D +1K-->1M 1K-->O -1K-->R -1L-->D -1L-->1N -1L-->O -1L-->7 -1N-->1C -1N-->1P -1N-->1R -1N-->1S -1N-->1T -1N-->D -1N-->O -1N-->7 -1N-->1O -1N-->J -1P-->D -1P-->O -1R-->1C -1R-->1S +1K-->7 +1M-->1E +1M-->1O +1M-->1Q +1M-->1R +1M-->1S +1M-->D +1M-->O +1M-->7 +1M-->1N +1M-->J +1O-->D +1O-->O +1Q-->1E +1Q-->1R +1Q-->O +1Q-->1N +1R-->D +1R-->Y 1R-->O -1R-->1O +1R-->1N +1S-->1Q 1S-->D -1S-->Y 1S-->O -1S-->1O -1T-->1R -1T-->D -1T-->O +1T-->Z 1U-->O diff --git a/dependency-graph.svg b/dependency-graph.svg index 842fbcdc..4a6d1a40 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,82 +4,82 @@ - - + + dependency-cruiser output - + cluster_fs - -fs + +fs cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/stacks/operations - -operations + +operations cluster_src/core/utils - -utils + +utils cluster_src/handlers - -handlers + +handlers cluster_src/handlers/modules - -modules + +modules cluster_~ - -~ + +~ cluster_~/typings - -typings + +typings bun:sqlite - -bun:sqlite + +bun:sqlite @@ -87,8 +87,8 @@ events - -events + +events @@ -96,8 +96,8 @@ fs - -fs + +fs @@ -105,8 +105,8 @@ fs/promises - -promises + +promises @@ -114,8 +114,8 @@ os - -os + +os @@ -123,8 +123,8 @@ package.json - -package.json + +package.json @@ -132,8 +132,8 @@ path - -path + +path @@ -141,8 +141,8 @@ src/core/database/_dbState.ts - -_dbState.ts + +_dbState.ts @@ -150,1382 +150,1354 @@ src/core/database/backup.ts - -backup.ts + +backup.ts src/core/database/backup.ts->fs - - + + src/core/database/backup.ts->src/core/database/_dbState.ts - - + + src/core/database/database.ts - -database.ts + +database.ts src/core/database/backup.ts->src/core/database/database.ts - - + + src/core/database/helper.ts - -helper.ts + +helper.ts src/core/database/backup.ts->src/core/database/helper.ts - - - - + + + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + ~/typings/misc - -misc + +misc src/core/database/backup.ts->~/typings/misc - - + + src/core/database/database.ts->bun:sqlite - - + + src/core/database/database.ts->fs - - + + src/core/database/database.ts->fs/promises - - + + src/core/database/database.ts->os - - + + src/core/database/database.ts->path - - + + src/core/database/helper.ts->src/core/database/_dbState.ts - - + + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + - + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + ~/typings/database - -database + +database - + src/core/utils/logger.ts->~/typings/database - - + + src/core/database/index.ts - -index.ts + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + - + src/handlers/modules/logs-socket.ts - - -logs-socket.ts + + +logs-socket.ts - + src/core/utils/logger.ts->src/handlers/modules/logs-socket.ts - - - - + + + + src/core/database/config.ts - -config.ts + +config.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts->~/typings/database - - + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + ~/typings/docker - -docker + +docker src/core/database/dockerHosts.ts->~/typings/docker - - + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts->~/typings/docker - - + + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + - + + +src/core/database/stores.ts + + +stores.ts + + + + +src/core/database/index.ts->src/core/database/stores.ts + + + + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + - + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/logs.ts->~/typings/database - - + + - + src/core/database/stacks.ts->src/core/database/database.ts - - + + - + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/stacks.ts->~/typings/database - - + + - + src/core/utils/helpers.ts - - -helpers.ts + + +helpers.ts - + src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - + + + + + + + +src/core/database/stores.ts->src/core/database/database.ts + + + + + +src/core/database/stores.ts->src/core/database/helper.ts + + + + - + src/core/utils/helpers.ts->src/core/utils/logger.ts - - - - + + - + src/core/docker/client.ts - - -client.ts + + +client.ts - + src/core/docker/client.ts->src/core/utils/logger.ts - - + + - + src/core/docker/client.ts->~/typings/docker - - + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - + src/core/docker/scheduler.ts->~/typings/database - - + + - + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + - + src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-container-stats.ts->~/typings/database - - + + - + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - - -calculations.ts + + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-host-stats.ts->~/typings/docker - - + + - + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + ~/typings/dockerode - - -dockerode + + +dockerode - + src/core/docker/store-host-stats.ts->~/typings/dockerode - - + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +plugin-manager.ts - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - - - + + + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->events - - + + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->~/typings/docker - - + + - + src/core/plugins/plugin-manager.ts->src/core/plugins/loader.ts - - - - + + + + - + ~/typings/plugin - - -plugin + + +plugin - + src/core/plugins/plugin-manager.ts->~/typings/plugin - - + + - + src/core/stacks/checker.ts - - -checker.ts + + +checker.ts - + src/core/stacks/checker.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/checker.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->~/typings/database - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts->src/core/stacks/checker.ts - - + + - + src/handlers/modules/docker-socket.ts - - -docker-socket.ts + + +docker-socket.ts - + src/core/stacks/controller.ts->src/handlers/modules/docker-socket.ts - - + + - + src/core/stacks/operations/runStackCommand.ts - - -runStackCommand.ts + + +runStackCommand.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/runStackCommand.ts - - + + - + src/core/stacks/operations/stackHelpers.ts - - -stackHelpers.ts + + +stackHelpers.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/stackHelpers.ts - - + + - + src/core/stacks/operations/stackStatus.ts - - -stackStatus.ts + + +stackStatus.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/stackStatus.ts - - + + - + ~/typings/docker-compose - - -docker-compose + + +docker-compose - + src/core/stacks/controller.ts->~/typings/docker-compose - - + + - + src/handlers/modules/docker-socket.ts->src/core/utils/logger.ts - - + + - + src/handlers/modules/docker-socket.ts->~/typings/database - - + + - + src/handlers/modules/docker-socket.ts->~/typings/docker - - + + - + src/handlers/modules/docker-socket.ts->src/core/database/index.ts - - + + - + src/handlers/modules/docker-socket.ts->src/core/docker/client.ts - - + + - + src/handlers/modules/docker-socket.ts->src/core/utils/calculations.ts - - + + - + src/handlers/modules/docker-socket.ts->src/handlers/modules/logs-socket.ts - - + + - + ~/typings/websocket - - -websocket + + +websocket - + src/handlers/modules/docker-socket.ts->~/typings/websocket - - + + - + src/core/stacks/operations/runStackCommand.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/operations/runStackCommand.ts->src/handlers/modules/docker-socket.ts - - + + - + src/core/stacks/operations/runStackCommand.ts->src/core/stacks/operations/stackHelpers.ts - - + + - + src/core/stacks/operations/runStackCommand.ts->~/typings/docker-compose - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/database/index.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/utils/helpers.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->~/typings/docker-compose - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/database/index.ts - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/stacks/operations/runStackCommand.ts - - + + - + src/handlers/modules/logs-socket.ts->src/core/utils/logger.ts - - - - + + + + - + src/handlers/modules/logs-socket.ts->~/typings/database - - + + stream - -stream + +stream - + src/handlers/modules/logs-socket.ts->stream - - + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - + src/handlers/config.ts - - -config.ts + + +config.ts - + src/handlers/config.ts->fs - - + + - + src/handlers/config.ts->src/core/database/backup.ts - - + + - + src/handlers/config.ts->src/core/utils/logger.ts - - + + - + src/handlers/config.ts->~/typings/database - - + + - + src/handlers/config.ts->~/typings/docker - - + + - + src/handlers/config.ts->src/core/database/index.ts - - + + - + src/handlers/config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/handlers/config.ts->~/typings/plugin - - + + - + src/handlers/config.ts->src/core/utils/package-json.ts - - + + - + src/handlers/database.ts - - -database.ts + + +database.ts - + src/handlers/database.ts->src/core/database/index.ts - - + + - + src/handlers/docker.ts - - -docker.ts + + +docker.ts - + src/handlers/docker.ts->src/core/utils/logger.ts - - + + - + src/handlers/docker.ts->~/typings/docker - - + + - + src/handlers/docker.ts->src/core/database/index.ts - - + + - + src/handlers/docker.ts->src/core/docker/client.ts - - + + - + src/handlers/docker.ts->~/typings/dockerode - - + + - + src/handlers/index.ts - - -index.ts + + +index.ts - - -src/handlers/index.ts->src/core/utils/logger.ts - - - - - -src/handlers/index.ts->src/core/docker/scheduler.ts - - - - - -src/handlers/index.ts->src/core/plugins/plugin-manager.ts - - - - - -src/handlers/index.ts->src/handlers/modules/docker-socket.ts - - - - + src/handlers/index.ts->src/handlers/config.ts - - + + - + src/handlers/index.ts->src/handlers/database.ts - - + + - + src/handlers/index.ts->src/handlers/docker.ts - - + + - + src/handlers/logs.ts - - -logs.ts + + +logs.ts - + src/handlers/index.ts->src/handlers/logs.ts - - + + - + src/handlers/modules/starter.ts - - -starter.ts + + +starter.ts - + src/handlers/index.ts->src/handlers/modules/starter.ts - - - - - -src/handlers/sockets.ts - - -sockets.ts - - - - - -src/handlers/index.ts->src/handlers/sockets.ts - - + + src/handlers/stacks.ts - -stacks.ts + +stacks.ts - + src/handlers/index.ts->src/handlers/stacks.ts - - + + - + +src/handlers/store.ts + + +store.ts + + + + + +src/handlers/index.ts->src/handlers/store.ts + + + + + src/handlers/utils.ts - - -utils.ts + + +utils.ts - + src/handlers/index.ts->src/handlers/utils.ts - - + + - + src/handlers/logs.ts->src/core/utils/logger.ts - - + + - + src/handlers/logs.ts->src/core/database/index.ts - - + + - + src/handlers/modules/starter.ts->src/core/docker/scheduler.ts - - + + - + src/handlers/modules/starter.ts->src/core/plugins/plugin-manager.ts - - + + - + src/handlers/modules/starter.ts->src/handlers/modules/docker-socket.ts - - - - - -src/handlers/sockets.ts->src/handlers/modules/docker-socket.ts - - - - - -src/handlers/sockets.ts->src/handlers/modules/logs-socket.ts - - - - - -src/handlers/modules/live-stacks.ts - - -live-stacks.ts - - - - - -src/handlers/sockets.ts->src/handlers/modules/live-stacks.ts - - + + - + src/handlers/stacks.ts->src/core/utils/logger.ts - - + + - + src/handlers/stacks.ts->~/typings/database - - + + - + src/handlers/stacks.ts->src/core/database/index.ts - - + + - + src/handlers/stacks.ts->src/core/stacks/controller.ts - - + + + + + +src/handlers/store.ts->src/core/database/stores.ts + + - + src/handlers/utils.ts->src/core/utils/logger.ts - - - - - -src/handlers/modules/live-stacks.ts->src/core/utils/logger.ts - - - - - -src/handlers/modules/live-stacks.ts->stream - - + + src/index.ts - -index.ts + +index.ts - + src/index.ts->src/handlers/index.ts - - + + From d3c645f85ae1ecee15cf114e2f0239370122e38c Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 10 Jul 2025 00:47:06 +0200 Subject: [PATCH 356/369] feat(database): Add theme support This commit introduces a new table to the database, allowing users to store and manage custom themes for the application. It also provides the ability to save a default theme on first start. The following changes were made: - Added a table to the database schema. - Added functions to interact with the table. - Add a Theme handler to expose theme actions --- docker/docker-compose.dev.yaml | 5 +- src/core/database/database.ts | 116 +++++++---- src/core/database/helper.ts | 44 ++--- src/core/database/index.ts | 18 +- src/core/database/themes.ts | 32 +++ src/handlers/config.ts | 346 +++++++++++++++++---------------- src/handlers/index.ts | 18 +- src/handlers/themes.ts | 27 +++ typings | 2 +- 9 files changed, 353 insertions(+), 255 deletions(-) create mode 100644 src/core/database/themes.ts create mode 100644 src/handlers/themes.ts diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index f302c585..7d4e6ca8 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -5,7 +5,7 @@ services: image: lscr.io/linuxserver/socket-proxy:latest volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - restart: unless-stopped + restart: never read_only: true tmpfs: - /run @@ -44,9 +44,10 @@ services: sqlite-web: container_name: sqlite-web image: ghcr.io/coleifer/sqlite-web:latest + restart: never ports: - 8080:8080 volumes: - - ../data:/data:ro + - /home/nik/Documents/Code-local/dockstat-project/DockStat/data:/data:ro environment: - SQLITE_DATABASE=dockstatapi.db diff --git a/src/core/database/database.ts b/src/core/database/database.ts index c5ee7c4f..059ce4f3 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -13,26 +13,26 @@ const uid = userInfo().uid; export let db: Database; try { - const databasePath = path.join(dataFolder, "dockstatapi.db"); - console.log("Database path:", databasePath); - console.log(`Running as: ${username} (${uid}:${gid})`); + const databasePath = path.join(dataFolder, "dockstatapi.db"); + console.log("Database path:", databasePath); + console.log(`Running as: ${username} (${uid}:${gid})`); - if (!existsSync(dataFolder)) { - await mkdir(dataFolder, { recursive: true, mode: 0o777 }); - console.log("Created data directory:", dataFolder); - } + if (!existsSync(dataFolder)) { + await mkdir(dataFolder, { recursive: true, mode: 0o777 }); + console.log("Created data directory:", dataFolder); + } - db = new Database(databasePath, { create: true }); - console.log("Database opened successfully"); + db = new Database(databasePath, { create: true }); + console.log("Database opened successfully"); - db.exec("PRAGMA journal_mode = WAL;"); + db.exec("PRAGMA journal_mode = WAL;"); } catch (error) { - console.error(`Cannot start DockStatAPI: ${error}`); - process.exit(500); + console.error(`Cannot start DockStatAPI: ${error}`); + process.exit(500); } export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -96,38 +96,68 @@ export function init() { slug TEXT NOT NULL, base TEXT NOT NULL ); + + CREATE TABLE IF NOT EXISTS themes ( + name TEXT NOT NULL, + creator TEXT NOT NULL, + vars TEXT NOT NULL, + tags TEXT NOT NULL + ) `); - const configRow = db - .prepare("SELECT COUNT(*) AS count FROM config") - .get() as { count: number }; - - if (configRow.count === 0) { - db.prepare( - "INSERT INTO config (keep_data_for, fetching_interval) VALUES (7, 5)", - ).run(); - } - - const hostRow = db - .prepare("SELECT COUNT(*) AS count FROM docker_hosts") - .get() as { count: number }; - - if (hostRow.count === 0) { - db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", - ).run("Localhost", "localhost:2375", false); - } - - const storeRow = db - .prepare("SELECT COUNT(*) AS count FROM store_repos") - .get() as { count: number }; - - if (storeRow.count === 0) { - db.prepare("INSERT INTO store_repos (slug, base) VALUES (?, ?)").run( - "DockStacks", - "https://raw.githubusercontent.com/Its4Nik/DockStacks/refs/heads/main/Index.json", - ); - } + const themeRows = db + .prepare("SELECT COUNT(*) AS count FROM themes") + .get() as { count: number }; + + const defaultCss = ` + .root, + #root, + #docs-root { + --accent: #f9a8d4; + --secondary-accent: #fbcfe8; + --border: #e5a3be; + --muted-bg: #fbeff3; + --gradient-from: #f8dbe2; + --gradient-to: #f6e3eb; + } + `; + + if (themeRows.count === 0) { + db.prepare( + "INSERT INTO themes (name, creator, vars, tags) VALUES (?,?,?,?)", + ).run("default", "Its4Nik", defaultCss, "[default]"); + } + + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; + + if (configRow.count === 0) { + db.prepare( + "INSERT INTO config (keep_data_for, fetching_interval) VALUES (7, 5)", + ).run(); + } + + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; + + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ).run("Localhost", "localhost:2375", false); + } + + const storeRow = db + .prepare("SELECT COUNT(*) AS count FROM store_repos") + .get() as { count: number }; + + if (storeRow.count === 0) { + db.prepare("INSERT INTO store_repos (slug, base) VALUES (?, ?)").run( + "DockStacks", + "https://raw.githubusercontent.com/Its4Nik/DockStacks/refs/heads/main/Index.json", + ); + } } init(); diff --git a/src/core/database/helper.ts b/src/core/database/helper.ts index 1f1cabd9..63291929 100644 --- a/src/core/database/helper.ts +++ b/src/core/database/helper.ts @@ -2,27 +2,27 @@ import { logger } from "~/core/utils/logger"; import { backupInProgress } from "./_dbState"; export function executeDbOperation( - label: string, - operation: () => T, - validate?: () => void, - dontLog?: boolean, + label: string, + operation: () => T, + validate?: () => void, + dontLog?: boolean, ): T { - if (backupInProgress && label !== "backup" && label !== "restore") { - throw new Error( - `backup in progress Database operation not allowed: ${label}`, - ); - } - const startTime = Date.now(); - if (dontLog !== true) { - logger.debug(`__task__ __db__ ${label} ⏳`); - } - if (validate) { - validate(); - } - const result = operation(); - const duration = Date.now() - startTime; - if (dontLog !== true) { - logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); - } - return result; + if (backupInProgress && label !== "backup" && label !== "restore") { + throw new Error( + `backup in progress Database operation not allowed: ${label}`, + ); + } + const startTime = Date.now(); + if (dontLog !== true) { + logger.debug(`__task__ __db__ ${label} ⏳`); + } + if (validate) { + validate(); + } + const result = operation(); + const duration = Date.now() - startTime; + if (dontLog !== true) { + logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); + } + return result; } diff --git a/src/core/database/index.ts b/src/core/database/index.ts index 8e2a9f0f..ca661818 100644 --- a/src/core/database/index.ts +++ b/src/core/database/index.ts @@ -10,16 +10,18 @@ import * as hostStats from "~/core/database/hostStats"; import * as logs from "~/core/database/logs"; import * as stacks from "~/core/database/stacks"; import * as stores from "~/core/database/stores"; +import * as themes from "~/core/database/themes"; export const dbFunctions = { - ...dockerHosts, - ...logs, - ...config, - ...containerStats, - ...hostStats, - ...stacks, - ...backup, - ...stores, + ...dockerHosts, + ...logs, + ...config, + ...containerStats, + ...hostStats, + ...stacks, + ...backup, + ...stores, + ...themes, }; export type dbFunctions = typeof dbFunctions; diff --git a/src/core/database/themes.ts b/src/core/database/themes.ts new file mode 100644 index 00000000..baa5b42c --- /dev/null +++ b/src/core/database/themes.ts @@ -0,0 +1,32 @@ +import type { Theme } from "~/typings/database"; +import { db } from "./database"; +import { executeDbOperation } from "./helper"; + +const stmt = { + insert: db.prepare(` + INSERT INTO themes (name, creator, vars, tags) VALUES (?, ?, ?, ?) + `), + remove: db.prepare(`DELETE FROM themes WHERE name = ?`), + read: db.prepare(`SELECT * FROM themes WHERE name = ?`), + readAll: db.prepare(`SELECT * FROM themes`), +}; + +export function getThemes() { + return executeDbOperation("Get Themes", () => stmt.readAll.all()) as Theme[]; +} + +export function addTheme({ name, creator, vars, tags }: Theme) { + return executeDbOperation("Save Theme", () => + stmt.insert.run(name, creator, vars, tags.toString()), + ); +} +export function getSpecificTheme(name: string): Theme { + return executeDbOperation( + "Getting specific Theme", + () => stmt.read.get(name) as Theme, + ); +} + +export function deleteTheme(name: string) { + return executeDbOperation("Remove Theme", () => stmt.remove.run(name)); +} diff --git a/src/handlers/config.ts b/src/handlers/config.ts index 62491ae3..971680c1 100644 --- a/src/handlers/config.ts +++ b/src/handlers/config.ts @@ -1,186 +1,190 @@ +import { PackageJson } from "knip/dist/types/package-json"; import { existsSync, readdirSync, unlinkSync } from "node:fs"; import { dbFunctions } from "~/core/database"; import { backupDir } from "~/core/database/backup"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import type { config } from "~/typings/database"; import type { DockerHost } from "~/typings/docker"; import type { PluginInfo } from "~/typings/plugin"; class apiHandler { - getConfig() { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateConfig(fetching_interval: number, keep_data_for: number) { - try { - dbFunctions.updateConfig(fetching_interval, keep_data_for); - return "Updated DockStatAPI config"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - getPlugins(): PluginInfo[] { - try { - logger.debug("Gathering plugins"); - return pluginManager.getPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getPackage() { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json`, - ); - - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } - - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async createbackup() { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return backupFilename; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async listBackups() { - try { - const backupFiles = readdirSync(backupDir); - - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.startsWith(".") || - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); - - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async downloadbackup(downloadFile?: string) { - try { - const filename: string = downloadFile || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; - - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } - - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async restoreBackup(file: Bun.FileBlob) { - try { - if (!file) { - throw new Error("No file uploaded"); - } - - if (!(file.name || "").endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } - - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); - - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); - - return "Database restored successfully"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async addHost(host: DockerHost) { - try { - dbFunctions.addDockerHost(host); - return `Added docker host (${host.name} - ${host.hostAddress})`; - } catch (error: unknown) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateHost(host: DockerHost) { - try { - dbFunctions.updateDockerHost(host); - return `Updated docker host (${host.id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async removeHost(id: number) { - try { - dbFunctions.deleteDockerHost(id); - return `Deleted docker host (${id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } + getConfig(): config { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateConfig(fetching_interval: number, keep_data_for: number) { + try { + logger.debug( + `Updated config: fetching_interval: ${fetching_interval} - keep_data_for: ${keep_data_for}`, + ); + dbFunctions.updateConfig(fetching_interval, keep_data_for); + return "Updated DockStatAPI config"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + getPlugins(): PluginInfo[] { + try { + logger.debug("Gathering plugins"); + return pluginManager.getPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async getPackage() { + try { + logger.debug("Fetching package.json"); + const data = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, + ); + + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } + + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async createbackup(): Promise { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return backupFilename; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async listBackups() { + try { + const backupFiles = readdirSync(backupDir); + + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.startsWith(".") || + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); + + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async downloadbackup(downloadFile?: string) { + try { + const filename: string = downloadFile || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; + + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } + + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async restoreBackup(file: File) { + try { + if (!file) { + throw new Error("No file uploaded"); + } + + if (!(file.name || "").endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } + + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return "Database restored successfully"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async addHost(host: DockerHost) { + try { + dbFunctions.addDockerHost(host); + return `Added docker host (${host.name} - ${host.hostAddress})`; + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateHost(host: DockerHost) { + try { + dbFunctions.updateDockerHost(host); + return `Updated docker host (${host.id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async removeHost(id: number) { + try { + dbFunctions.deleteDockerHost(id); + return `Deleted docker host (${id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } } export const ApiHandler = new apiHandler(); diff --git a/src/handlers/index.ts b/src/handlers/index.ts index 7999857d..d477548a 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -5,17 +5,19 @@ import { LogHandler } from "./logs"; import { Starter } from "./modules/starter"; import { StackHandler } from "./stacks"; import { StoreHandler } from "./store"; +import { ThemeHandler } from "./themes"; import { CheckHealth } from "./utils"; export const handlers = { - BasicDockerHandler, - ApiHandler, - DatabaseHandler, - StackHandler, - LogHandler, - CheckHealth, - Socket: "ws://localhost:4837/ws", - StoreHandler, + BasicDockerHandler, + ApiHandler, + DatabaseHandler, + StackHandler, + LogHandler, + CheckHealth, + Socket: "ws://localhost:4837/ws", + StoreHandler, + ThemeHandler, }; Starter.startAll(); diff --git a/src/handlers/themes.ts b/src/handlers/themes.ts new file mode 100644 index 00000000..5e61655f --- /dev/null +++ b/src/handlers/themes.ts @@ -0,0 +1,27 @@ +import { dbFunctions } from "~/core/database"; +import type { Theme } from "~/typings/database"; + +class themeHandler { + getThemes(): Theme[] { + return dbFunctions.getThemes(); + } + addTheme(theme: Theme) { + try { + return dbFunctions.addTheme({ ...theme }); + } catch (error) { + throw new Error(`Could not save theme ${theme}, error: ${error}`); + } + } + deleteTheme({ name }: Theme) { + try { + return dbFunctions.deleteTheme(name); + } catch (error) { + throw new Error(`Could not save theme ${name}, error: ${error}`); + } + } + getTheme(name: string): Theme { + return dbFunctions.getSpecificTheme(name); + } +} + +export const ThemeHandler = new themeHandler(); diff --git a/typings b/typings index 01123928..aba9f6b7 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 01123928a672ac823b8371114fae75beca3f2442 +Subproject commit aba9f6b74c0c63998186672ff45a843e984a93bd From 4ac7e14de2608e6f88d024915597ce43a7062a7e Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 10 Jul 2025 00:52:56 +0200 Subject: [PATCH 357/369] feat(style): Update theme colors to dark mode Updates the theme colors to a dark mode palette. This includes changes to accent colors, border colors, background colors, and gradient colors. --- src/core/database/database.ts | 12 ++++++------ typings | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/database/database.ts b/src/core/database/database.ts index 059ce4f3..cd537237 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -113,12 +113,12 @@ export function init() { .root, #root, #docs-root { - --accent: #f9a8d4; - --secondary-accent: #fbcfe8; - --border: #e5a3be; - --muted-bg: #fbeff3; - --gradient-from: #f8dbe2; - --gradient-to: #f6e3eb; + --accent: #818cf8; + --secondary-accent: #a5b4fc; + --border: #4b5563; + --muted-bg: #18212f; + --gradient-from: #1f2937; + --gradient-to: #111827; } `; diff --git a/typings b/typings index aba9f6b7..e69df215 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit aba9f6b74c0c63998186672ff45a843e984a93bd +Subproject commit e69df2154d40a533694ee810891fecfc88440ff9 From 503db973f8fd8c5368871df74acaf096a37a48dc Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 10 Jul 2025 11:33:54 +0200 Subject: [PATCH 358/369] feat(themes): Transform and save CSS variables for themes This commit introduces a transformation of the `vars` field in the `addTheme` function. It converts a JSON object of CSS variables into a string of CSS rules that target the root element. Additionally, it updates the themes table to set the name field as the primary key. This change is crucial for ensuring data integrity and efficient theme management. --- src/core/database/database.ts | 2 +- src/handlers/themes.ts | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/core/database/database.ts b/src/core/database/database.ts index cd537237..80fdc851 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -98,7 +98,7 @@ export function init() { ); CREATE TABLE IF NOT EXISTS themes ( - name TEXT NOT NULL, + name TEXT PRIMARY KEY, creator TEXT NOT NULL, vars TEXT NOT NULL, tags TEXT NOT NULL diff --git a/src/handlers/themes.ts b/src/handlers/themes.ts index 5e61655f..40677b57 100644 --- a/src/handlers/themes.ts +++ b/src/handlers/themes.ts @@ -7,9 +7,23 @@ class themeHandler { } addTheme(theme: Theme) { try { - return dbFunctions.addTheme({ ...theme }); + const rawVars = + typeof theme.vars === "string" ? JSON.parse(theme.vars) : theme.vars; + + const cssVars = Object.entries(rawVars) + .map(([key, value]) => `--${key}: ${value};`) + .join(" "); + + const varsString = `.root, #root, #docs-root { ${cssVars} }`; + + return dbFunctions.addTheme({ + ...theme, + vars: varsString, + }); } catch (error) { - throw new Error(`Could not save theme ${theme}, error: ${error}`); + throw new Error( + `Could not save theme ${JSON.stringify(theme)}, error: ${error}`, + ); } } deleteTheme({ name }: Theme) { From 93870b42d0cdae4598bed80a9d77bfd48b80a439 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 10 Jul 2025 09:34:29 +0000 Subject: [PATCH 359/369] Update dependency graphs --- dependency-graph.mmd | 215 +++---- dependency-graph.svg | 1330 ++++++++++++++++++++++-------------------- 2 files changed, 809 insertions(+), 736 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 95a9101d..8108251e 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -13,15 +13,16 @@ subgraph 2["handlers"] 4["config.ts"] subgraph P["modules"] Q["logs-socket.ts"] -1D["starter.ts"] -1E["docker-socket.ts"] +1E["starter.ts"] +1F["docker-socket.ts"] end -17["database.ts"] -18["docker.ts"] -1C["logs.ts"] -1K["stacks.ts"] -1T["store.ts"] -1U["utils.ts"] +18["database.ts"] +19["docker.ts"] +1D["logs.ts"] +1L["stacks.ts"] +1U["store.ts"] +1V["themes.ts"] +1W["utils.ts"] end subgraph B["core"] subgraph C["database"] @@ -37,31 +38,32 @@ V["hostStats.ts"] W["logs.ts"] X["stacks.ts"] Z["stores.ts"] +10["themes.ts"] end subgraph N["utils"] O["logger.ts"] Y["helpers.ts"] -14["change-me-checker.ts"] -15["package-json.ts"] -1G["calculations.ts"] +15["change-me-checker.ts"] +16["package-json.ts"] +1H["calculations.ts"] end -subgraph 10["plugins"] -11["plugin-manager.ts"] -13["loader.ts"] +subgraph 11["plugins"] +12["plugin-manager.ts"] +14["loader.ts"] end -subgraph 1A["docker"] -1B["client.ts"] -1H["scheduler.ts"] -1I["store-container-stats.ts"] -1J["store-host-stats.ts"] +subgraph 1B["docker"] +1C["client.ts"] +1I["scheduler.ts"] +1J["store-container-stats.ts"] +1K["store-host-stats.ts"] end -subgraph 1L["stacks"] -1M["controller.ts"] -1O["checker.ts"] -subgraph 1P["operations"] -1Q["runStackCommand.ts"] -1R["stackHelpers.ts"] -1S["stackStatus.ts"] +subgraph 1M["stacks"] +1N["controller.ts"] +1P["checker.ts"] +subgraph 1Q["operations"] +1R["runStackCommand.ts"] +1S["stackHelpers.ts"] +1T["stackStatus.ts"] end end end @@ -72,9 +74,9 @@ subgraph 6["typings"] 8["docker"] 9["plugin"] F["misc"] -19["dockerode"] -1F["websocket"] -1N["docker-compose"] +1A["dockerode"] +1G["websocket"] +1O["docker-compose"] end end subgraph A["fs"] @@ -84,22 +86,23 @@ I["bun:sqlite"] K["os"] L["path"] R["stream"] -12["events"] -16["package.json"] +13["events"] +17["package.json"] 1-->3 3-->4 -3-->17 3-->18 -3-->1C +3-->19 3-->1D -3-->1K -3-->1T +3-->1E +3-->1L 3-->1U +3-->1V +3-->1W 4-->D 4-->E -4-->11 +4-->12 4-->O -4-->15 +4-->16 4-->7 4-->8 4-->9 @@ -113,6 +116,7 @@ D-->V D-->W D-->X D-->Z +D-->10 E-->G E-->H E-->M @@ -155,82 +159,87 @@ X-->7 Y-->O Z-->H Z-->M -11-->O -11-->13 -11-->8 -11-->9 -11-->12 -13-->14 -13-->O -13-->11 -13-->A -13-->L +10-->H +10-->M +10-->7 +12-->O +12-->14 +12-->8 +12-->9 +12-->13 +14-->15 14-->O -14-->J -15-->16 -17-->D +14-->12 +14-->A +14-->L +15-->O +15-->J +16-->17 18-->D -18-->1B -18-->O -18-->8 -18-->19 -1B-->O -1B-->8 -1C-->D +19-->D +19-->1C +19-->O +19-->8 +19-->1A 1C-->O -1D-->1E -1D-->1H -1D-->11 -1E-->Q -1E-->D -1E-->1B -1E-->1G -1E-->O -1E-->7 -1E-->8 +1C-->8 +1D-->D +1D-->O 1E-->1F -1H-->D -1H-->1I -1H-->1J -1H-->O -1H-->7 -1I-->O +1E-->1I +1E-->12 +1F-->Q +1F-->D +1F-->1C +1F-->1H +1F-->O +1F-->7 +1F-->8 +1F-->1G 1I-->D -1I-->1B -1I-->1G +1I-->1J +1I-->1K +1I-->O 1I-->7 -1J-->D -1J-->1B 1J-->O -1J-->8 -1J-->19 +1J-->D +1J-->1C +1J-->1H +1J-->7 1K-->D -1K-->1M +1K-->1C 1K-->O -1K-->7 -1M-->1E -1M-->1O -1M-->1Q -1M-->1R -1M-->1S -1M-->D -1M-->O -1M-->7 -1M-->1N -1M-->J -1O-->D -1O-->O -1Q-->1E -1Q-->1R -1Q-->O -1Q-->1N -1R-->D -1R-->Y +1K-->8 +1K-->1A +1L-->D +1L-->1N +1L-->O +1L-->7 +1N-->1F +1N-->1P +1N-->1R +1N-->1S +1N-->1T +1N-->D +1N-->O +1N-->7 +1N-->1O +1N-->J +1P-->D +1P-->O +1R-->1F +1R-->1S 1R-->O -1R-->1N -1S-->1Q +1R-->1O 1S-->D +1S-->Y 1S-->O -1T-->Z -1U-->O +1S-->1O +1T-->1R +1T-->D +1T-->O +1U-->Z +1V-->D +1V-->7 +1W-->O diff --git a/dependency-graph.svg b/dependency-graph.svg index 4a6d1a40..84e6375e 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,11 +4,11 @@ - - + + dependency-cruiser output - + cluster_fs @@ -16,53 +16,53 @@ cluster_src - -src + +src cluster_src/core - -core + +core cluster_src/core/database - -database + +database cluster_src/core/docker - -docker + +docker cluster_src/core/plugins - -plugins + +plugins cluster_src/core/stacks - -stacks + +stacks cluster_src/core/stacks/operations - -operations + +operations cluster_src/core/utils - -utils + +utils cluster_src/handlers - -handlers + +handlers cluster_src/handlers/modules - -modules + +modules cluster_~ @@ -114,8 +114,8 @@ os - -os + +os @@ -132,8 +132,8 @@ path - -path + +path @@ -141,8 +141,8 @@ src/core/database/_dbState.ts - -_dbState.ts + +_dbState.ts @@ -150,142 +150,142 @@ src/core/database/backup.ts - -backup.ts + +backup.ts src/core/database/backup.ts->fs - - + + src/core/database/backup.ts->src/core/database/_dbState.ts - - + + src/core/database/database.ts - -database.ts + +database.ts src/core/database/backup.ts->src/core/database/database.ts - - + + src/core/database/helper.ts - -helper.ts + +helper.ts src/core/database/backup.ts->src/core/database/helper.ts - - - - + + + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + ~/typings/misc - -misc + +misc src/core/database/backup.ts->~/typings/misc - - + + src/core/database/database.ts->bun:sqlite - - + + src/core/database/database.ts->fs - - + + src/core/database/database.ts->fs/promises - - + + src/core/database/database.ts->os - - + + src/core/database/database.ts->path - - + + src/core/database/helper.ts->src/core/database/_dbState.ts - - + + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - - + + - + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + @@ -297,119 +297,119 @@ - + src/core/utils/logger.ts->~/typings/database - - + + src/core/database/index.ts - -index.ts + +index.ts - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + - + src/handlers/modules/logs-socket.ts - - -logs-socket.ts + + +logs-socket.ts - + src/core/utils/logger.ts->src/handlers/modules/logs-socket.ts - - - - + + + + src/core/database/config.ts - -config.ts + +config.ts src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts->~/typings/database - - + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + @@ -423,1081 +423,1145 @@ src/core/database/dockerHosts.ts->~/typings/docker - - + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts->~/typings/docker - - + + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/stores.ts - -stores.ts + +stores.ts src/core/database/index.ts->src/core/database/stores.ts - - - - + + + + - + + +src/core/database/themes.ts + + +themes.ts + + + + +src/core/database/index.ts->src/core/database/themes.ts + + + + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + - + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/logs.ts->~/typings/database - - + + - + src/core/database/stacks.ts->src/core/database/database.ts - - + + - + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/stacks.ts->~/typings/database - - + + - + src/core/utils/helpers.ts - - -helpers.ts + + +helpers.ts - + src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - + + + + - + src/core/database/stores.ts->src/core/database/database.ts - - + + - + src/core/database/stores.ts->src/core/database/helper.ts - - - - + + + + + + + +src/core/database/themes.ts->src/core/database/database.ts + + + + + +src/core/database/themes.ts->src/core/database/helper.ts + + + + + + + +src/core/database/themes.ts->~/typings/database + + - + src/core/utils/helpers.ts->src/core/utils/logger.ts - - + + - + src/core/docker/client.ts - - -client.ts + + +client.ts - + src/core/docker/client.ts->src/core/utils/logger.ts - - + + - + src/core/docker/client.ts->~/typings/docker - - + + - + src/core/docker/scheduler.ts - - -scheduler.ts + + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - + src/core/docker/scheduler.ts->~/typings/database - - + + - + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + - + src/core/docker/store-container-stats.ts - - -store-container-stats.ts + + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + - + src/core/docker/store-host-stats.ts - - -store-host-stats.ts + + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-container-stats.ts->~/typings/database - - + + - + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + - + src/core/utils/calculations.ts - - -calculations.ts + + +calculations.ts - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-host-stats.ts->~/typings/docker - - + + - + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + - + ~/typings/dockerode - - -dockerode + + +dockerode - + src/core/docker/store-host-stats.ts->~/typings/dockerode - - + + - + src/core/plugins/loader.ts - - -loader.ts + + +loader.ts - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + - + src/core/utils/change-me-checker.ts - - -change-me-checker.ts + + +change-me-checker.ts - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + - + src/core/plugins/plugin-manager.ts - - -plugin-manager.ts + + +plugin-manager.ts - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - - - + + + + - + src/core/utils/change-me-checker.ts->fs/promises - - + + - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->events - + - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->~/typings/docker - - + + - + src/core/plugins/plugin-manager.ts->src/core/plugins/loader.ts - - - - + + + + - + ~/typings/plugin - - -plugin + + +plugin - + src/core/plugins/plugin-manager.ts->~/typings/plugin - - + + - + src/core/stacks/checker.ts - - -checker.ts + + +checker.ts - + src/core/stacks/checker.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/checker.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts - - -controller.ts + + +controller.ts - + src/core/stacks/controller.ts->fs/promises - + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->~/typings/database - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts->src/core/stacks/checker.ts - - + + - + src/handlers/modules/docker-socket.ts - - -docker-socket.ts + + +docker-socket.ts - + src/core/stacks/controller.ts->src/handlers/modules/docker-socket.ts - - + + - + src/core/stacks/operations/runStackCommand.ts - - -runStackCommand.ts + + +runStackCommand.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/runStackCommand.ts - - + + - + src/core/stacks/operations/stackHelpers.ts - - -stackHelpers.ts + + +stackHelpers.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/stackHelpers.ts - - + + - + src/core/stacks/operations/stackStatus.ts - - -stackStatus.ts + + +stackStatus.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/stackStatus.ts - - + + - + ~/typings/docker-compose - + docker-compose - + src/core/stacks/controller.ts->~/typings/docker-compose - - + + - + src/handlers/modules/docker-socket.ts->src/core/utils/logger.ts - - + + - + src/handlers/modules/docker-socket.ts->~/typings/database - - + + - + src/handlers/modules/docker-socket.ts->~/typings/docker - - + + - + src/handlers/modules/docker-socket.ts->src/core/database/index.ts - - + + - + src/handlers/modules/docker-socket.ts->src/core/docker/client.ts - - + + - + src/handlers/modules/docker-socket.ts->src/core/utils/calculations.ts - - + + - + src/handlers/modules/docker-socket.ts->src/handlers/modules/logs-socket.ts - - + + - + ~/typings/websocket - - -websocket + + +websocket - + src/handlers/modules/docker-socket.ts->~/typings/websocket - - + + - + src/core/stacks/operations/runStackCommand.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/operations/runStackCommand.ts->src/handlers/modules/docker-socket.ts - - + + - + src/core/stacks/operations/runStackCommand.ts->src/core/stacks/operations/stackHelpers.ts - - + + - + src/core/stacks/operations/runStackCommand.ts->~/typings/docker-compose - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/database/index.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/utils/helpers.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->~/typings/docker-compose - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/database/index.ts - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/stacks/operations/runStackCommand.ts - - + + - + src/handlers/modules/logs-socket.ts->src/core/utils/logger.ts - - - - + + + + - + src/handlers/modules/logs-socket.ts->~/typings/database - - + + - + stream - + stream - + src/handlers/modules/logs-socket.ts->stream - - + + - + src/core/utils/package-json.ts - - -package-json.ts + + +package-json.ts - + src/core/utils/package-json.ts->package.json - - + + - + src/handlers/config.ts - - -config.ts + + +config.ts - + src/handlers/config.ts->fs - + - + src/handlers/config.ts->src/core/database/backup.ts - - + + - + src/handlers/config.ts->src/core/utils/logger.ts - - + + - + src/handlers/config.ts->~/typings/database - - + + - + src/handlers/config.ts->~/typings/docker - - + + - + src/handlers/config.ts->src/core/database/index.ts - - + + - + src/handlers/config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/handlers/config.ts->~/typings/plugin - - + + - + src/handlers/config.ts->src/core/utils/package-json.ts - - + + - + src/handlers/database.ts - - -database.ts + + +database.ts - + src/handlers/database.ts->src/core/database/index.ts - - + + - + src/handlers/docker.ts - - -docker.ts + + +docker.ts - + src/handlers/docker.ts->src/core/utils/logger.ts - - + + - + src/handlers/docker.ts->~/typings/docker - - + + - + src/handlers/docker.ts->src/core/database/index.ts - - + + - + src/handlers/docker.ts->src/core/docker/client.ts - - + + - + src/handlers/docker.ts->~/typings/dockerode - - + + - + src/handlers/index.ts - - -index.ts + + +index.ts - + src/handlers/index.ts->src/handlers/config.ts - - + + - + src/handlers/index.ts->src/handlers/database.ts - - + + - + src/handlers/index.ts->src/handlers/docker.ts - - + + - + src/handlers/logs.ts - - -logs.ts + + +logs.ts - + src/handlers/index.ts->src/handlers/logs.ts - - + + - + src/handlers/modules/starter.ts - - -starter.ts + + +starter.ts - + src/handlers/index.ts->src/handlers/modules/starter.ts - - + + - + src/handlers/stacks.ts - - -stacks.ts + + +stacks.ts - + src/handlers/index.ts->src/handlers/stacks.ts - - + + - + src/handlers/store.ts - - -store.ts + + +store.ts - + src/handlers/index.ts->src/handlers/store.ts - - + + + + + +src/handlers/themes.ts + + +themes.ts + + + + + +src/handlers/index.ts->src/handlers/themes.ts + + - + src/handlers/utils.ts - - -utils.ts + + +utils.ts - + src/handlers/index.ts->src/handlers/utils.ts - - + + - + src/handlers/logs.ts->src/core/utils/logger.ts - - + + - + src/handlers/logs.ts->src/core/database/index.ts - - + + - + src/handlers/modules/starter.ts->src/core/docker/scheduler.ts - - + + - + src/handlers/modules/starter.ts->src/core/plugins/plugin-manager.ts - - + + - + src/handlers/modules/starter.ts->src/handlers/modules/docker-socket.ts - - + + - + src/handlers/stacks.ts->src/core/utils/logger.ts - - + + - + src/handlers/stacks.ts->~/typings/database - - + + - + src/handlers/stacks.ts->src/core/database/index.ts - - + + - + src/handlers/stacks.ts->src/core/stacks/controller.ts - - + + - + src/handlers/store.ts->src/core/database/stores.ts - - + + + + + +src/handlers/themes.ts->~/typings/database + + + + + +src/handlers/themes.ts->src/core/database/index.ts + + - + src/handlers/utils.ts->src/core/utils/logger.ts - - + + - + src/index.ts - - -index.ts + + +index.ts - + src/index.ts->src/handlers/index.ts - - + + From b7eb60a6c6e6c3730d539b022d4aa2c794b5dbd9 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 10 Jul 2025 12:27:37 +0200 Subject: [PATCH 360/369] feat(styles): Add text color variables to CSS root This commit introduces new CSS variables for text colors: `--text-primary`, `--text-secondary`, and `--text-muted`. These variables are defined within the `:root, #root, #docs-root` CSS selector and provide a centralized way to manage text colors across the application. This will improve consistency and make it easier to adjust the color scheme in the future. --- src/core/database/database.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/core/database/database.ts b/src/core/database/database.ts index 80fdc851..9ad8f4d6 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -113,12 +113,15 @@ export function init() { .root, #root, #docs-root { - --accent: #818cf8; - --secondary-accent: #a5b4fc; - --border: #4b5563; - --muted-bg: #18212f; - --gradient-from: #1f2937; - --gradient-to: #111827; + --accent: #818cf8; + --secondary-accent: #a5b4fc; + --text-primary: #f3f4f6; + --text-secondary: #d1d5db; + --text-muted: #9ca3af; + --border: #4b5563; + --muted-bg: #18212f; + --gradient-from: #1f2937; + --gradient-to: #111827; } `; From 2f21ccda9172d545c0d01fa5f2ae43e82b48afb0 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 11 Jul 2025 15:20:03 +0200 Subject: [PATCH 361/369] feat(config): Enhance config handling and add package.json retrieval This commit introduces several enhancements to the configuration handling and adds functionality to retrieve the package.json data. The changes include: - Update config to use package.json - Enhanced config handling in `src/handlers/config.ts`: - Updated the getConfig, updateConfig, getPlugins, createbackup, listBackups, downloadbackup, restoreBackup, addHost, updateHost, and removeHost functions to improve error handling and logging. - Added a new getPackage function to retrieve package.json data. - Updated the index file to export new handlers --- src/core/database/database.ts | 106 +++++----- src/core/database/helper.ts | 44 ++--- src/core/database/index.ts | 18 +- src/core/database/themes.ts | 26 +-- src/handlers/config.ts | 360 +++++++++++++++++----------------- src/handlers/index.ts | 18 +- src/handlers/themes.ts | 62 +++--- src/handlers/utils.ts | 2 +- 8 files changed, 323 insertions(+), 313 deletions(-) diff --git a/src/core/database/database.ts b/src/core/database/database.ts index 9ad8f4d6..3b449ad2 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -13,26 +13,26 @@ const uid = userInfo().uid; export let db: Database; try { - const databasePath = path.join(dataFolder, "dockstatapi.db"); - console.log("Database path:", databasePath); - console.log(`Running as: ${username} (${uid}:${gid})`); + const databasePath = path.join(dataFolder, "dockstatapi.db"); + console.log("Database path:", databasePath); + console.log(`Running as: ${username} (${uid}:${gid})`); - if (!existsSync(dataFolder)) { - await mkdir(dataFolder, { recursive: true, mode: 0o777 }); - console.log("Created data directory:", dataFolder); - } + if (!existsSync(dataFolder)) { + await mkdir(dataFolder, { recursive: true, mode: 0o777 }); + console.log("Created data directory:", dataFolder); + } - db = new Database(databasePath, { create: true }); - console.log("Database opened successfully"); + db = new Database(databasePath, { create: true }); + console.log("Database opened successfully"); - db.exec("PRAGMA journal_mode = WAL;"); + db.exec("PRAGMA journal_mode = WAL;"); } catch (error) { - console.error(`Cannot start DockStatAPI: ${error}`); - process.exit(500); + console.error(`Cannot start DockStatAPI: ${error}`); + process.exit(500); } export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -105,11 +105,11 @@ export function init() { ) `); - const themeRows = db - .prepare("SELECT COUNT(*) AS count FROM themes") - .get() as { count: number }; + const themeRows = db + .prepare("SELECT COUNT(*) AS count FROM themes") + .get() as { count: number }; - const defaultCss = ` + const defaultCss = ` .root, #root, #docs-root { @@ -125,42 +125,42 @@ export function init() { } `; - if (themeRows.count === 0) { - db.prepare( - "INSERT INTO themes (name, creator, vars, tags) VALUES (?,?,?,?)", - ).run("default", "Its4Nik", defaultCss, "[default]"); - } - - const configRow = db - .prepare("SELECT COUNT(*) AS count FROM config") - .get() as { count: number }; - - if (configRow.count === 0) { - db.prepare( - "INSERT INTO config (keep_data_for, fetching_interval) VALUES (7, 5)", - ).run(); - } - - const hostRow = db - .prepare("SELECT COUNT(*) AS count FROM docker_hosts") - .get() as { count: number }; - - if (hostRow.count === 0) { - db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", - ).run("Localhost", "localhost:2375", false); - } - - const storeRow = db - .prepare("SELECT COUNT(*) AS count FROM store_repos") - .get() as { count: number }; - - if (storeRow.count === 0) { - db.prepare("INSERT INTO store_repos (slug, base) VALUES (?, ?)").run( - "DockStacks", - "https://raw.githubusercontent.com/Its4Nik/DockStacks/refs/heads/main/Index.json", - ); - } + if (themeRows.count === 0) { + db.prepare( + "INSERT INTO themes (name, creator, vars, tags) VALUES (?,?,?,?)", + ).run("default", "Its4Nik", defaultCss, "[default]"); + } + + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; + + if (configRow.count === 0) { + db.prepare( + "INSERT INTO config (keep_data_for, fetching_interval) VALUES (7, 5)", + ).run(); + } + + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; + + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ).run("Localhost", "localhost:2375", false); + } + + const storeRow = db + .prepare("SELECT COUNT(*) AS count FROM store_repos") + .get() as { count: number }; + + if (storeRow.count === 0) { + db.prepare("INSERT INTO store_repos (slug, base) VALUES (?, ?)").run( + "DockStacks", + "https://raw.githubusercontent.com/Its4Nik/DockStacks/refs/heads/main/Index.json", + ); + } } init(); diff --git a/src/core/database/helper.ts b/src/core/database/helper.ts index 63291929..1f1cabd9 100644 --- a/src/core/database/helper.ts +++ b/src/core/database/helper.ts @@ -2,27 +2,27 @@ import { logger } from "~/core/utils/logger"; import { backupInProgress } from "./_dbState"; export function executeDbOperation( - label: string, - operation: () => T, - validate?: () => void, - dontLog?: boolean, + label: string, + operation: () => T, + validate?: () => void, + dontLog?: boolean, ): T { - if (backupInProgress && label !== "backup" && label !== "restore") { - throw new Error( - `backup in progress Database operation not allowed: ${label}`, - ); - } - const startTime = Date.now(); - if (dontLog !== true) { - logger.debug(`__task__ __db__ ${label} ⏳`); - } - if (validate) { - validate(); - } - const result = operation(); - const duration = Date.now() - startTime; - if (dontLog !== true) { - logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); - } - return result; + if (backupInProgress && label !== "backup" && label !== "restore") { + throw new Error( + `backup in progress Database operation not allowed: ${label}`, + ); + } + const startTime = Date.now(); + if (dontLog !== true) { + logger.debug(`__task__ __db__ ${label} ⏳`); + } + if (validate) { + validate(); + } + const result = operation(); + const duration = Date.now() - startTime; + if (dontLog !== true) { + logger.debug(`__task__ __db__ ${label} ✔️ (${duration}ms)`); + } + return result; } diff --git a/src/core/database/index.ts b/src/core/database/index.ts index ca661818..7bc61473 100644 --- a/src/core/database/index.ts +++ b/src/core/database/index.ts @@ -13,15 +13,15 @@ import * as stores from "~/core/database/stores"; import * as themes from "~/core/database/themes"; export const dbFunctions = { - ...dockerHosts, - ...logs, - ...config, - ...containerStats, - ...hostStats, - ...stacks, - ...backup, - ...stores, - ...themes, + ...dockerHosts, + ...logs, + ...config, + ...containerStats, + ...hostStats, + ...stacks, + ...backup, + ...stores, + ...themes, }; export type dbFunctions = typeof dbFunctions; diff --git a/src/core/database/themes.ts b/src/core/database/themes.ts index baa5b42c..94dd42eb 100644 --- a/src/core/database/themes.ts +++ b/src/core/database/themes.ts @@ -3,30 +3,30 @@ import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare(` + insert: db.prepare(` INSERT INTO themes (name, creator, vars, tags) VALUES (?, ?, ?, ?) `), - remove: db.prepare(`DELETE FROM themes WHERE name = ?`), - read: db.prepare(`SELECT * FROM themes WHERE name = ?`), - readAll: db.prepare(`SELECT * FROM themes`), + remove: db.prepare("DELETE FROM themes WHERE name = ?"), + read: db.prepare("SELECT * FROM themes WHERE name = ?"), + readAll: db.prepare("SELECT * FROM themes"), }; export function getThemes() { - return executeDbOperation("Get Themes", () => stmt.readAll.all()) as Theme[]; + return executeDbOperation("Get Themes", () => stmt.readAll.all()) as Theme[]; } export function addTheme({ name, creator, vars, tags }: Theme) { - return executeDbOperation("Save Theme", () => - stmt.insert.run(name, creator, vars, tags.toString()), - ); + return executeDbOperation("Save Theme", () => + stmt.insert.run(name, creator, vars, tags.toString()), + ); } export function getSpecificTheme(name: string): Theme { - return executeDbOperation( - "Getting specific Theme", - () => stmt.read.get(name) as Theme, - ); + return executeDbOperation( + "Getting specific Theme", + () => stmt.read.get(name) as Theme, + ); } export function deleteTheme(name: string) { - return executeDbOperation("Remove Theme", () => stmt.remove.run(name)); + return executeDbOperation("Remove Theme", () => stmt.remove.run(name)); } diff --git a/src/handlers/config.ts b/src/handlers/config.ts index 971680c1..1bd52d9d 100644 --- a/src/handlers/config.ts +++ b/src/handlers/config.ts @@ -1,190 +1,200 @@ -import { PackageJson } from "knip/dist/types/package-json"; import { existsSync, readdirSync, unlinkSync } from "node:fs"; +import { PackageJson } from "knip/dist/types/package-json"; import { dbFunctions } from "~/core/database"; import { backupDir } from "~/core/database/backup"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import type { config } from "~/typings/database"; import type { DockerHost } from "~/typings/docker"; import type { PluginInfo } from "~/typings/plugin"; class apiHandler { - getConfig(): config { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateConfig(fetching_interval: number, keep_data_for: number) { - try { - logger.debug( - `Updated config: fetching_interval: ${fetching_interval} - keep_data_for: ${keep_data_for}`, - ); - dbFunctions.updateConfig(fetching_interval, keep_data_for); - return "Updated DockStatAPI config"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - getPlugins(): PluginInfo[] { - try { - logger.debug("Gathering plugins"); - return pluginManager.getPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async getPackage() { - try { - logger.debug("Fetching package.json"); - const data = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json`, - ); - - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } - - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async createbackup(): Promise { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return backupFilename; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async listBackups() { - try { - const backupFiles = readdirSync(backupDir); - - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.startsWith(".") || - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); - - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async downloadbackup(downloadFile?: string) { - try { - const filename: string = downloadFile || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; - - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } - - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async restoreBackup(file: File) { - try { - if (!file) { - throw new Error("No file uploaded"); - } - - if (!(file.name || "").endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } - - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); - - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); - - return "Database restored successfully"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async addHost(host: DockerHost) { - try { - dbFunctions.addDockerHost(host); - return `Added docker host (${host.name} - ${host.hostAddress})`; - } catch (error: unknown) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateHost(host: DockerHost) { - try { - dbFunctions.updateDockerHost(host); - return `Updated docker host (${host.id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async removeHost(id: number) { - try { - dbFunctions.deleteDockerHost(id); - return `Deleted docker host (${id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } + getConfig(): config { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateConfig(fetching_interval: number, keep_data_for: number) { + try { + logger.debug( + `Updated config: fetching_interval: ${fetching_interval} - keep_data_for: ${keep_data_for}`, + ); + dbFunctions.updateConfig(fetching_interval, keep_data_for); + return "Updated DockStatAPI config"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + getPlugins(): PluginInfo[] { + try { + logger.debug("Gathering plugins"); + return pluginManager.getPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + getPackage() { + try { + logger.debug("Fetching package.json"); + const data: { + version: string; + description: string; + license: string; + authorName: string; + authorEmail: string; + authorWebsite: string; + contributors: string[]; + dependencies: Record; + devDependencies: Record; + } = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, + ); + + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } + + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async createbackup(): Promise { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return backupFilename; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async listBackups() { + try { + const backupFiles = readdirSync(backupDir); + + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.startsWith(".") || + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); + + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async downloadbackup(downloadFile?: string) { + try { + const filename: string = downloadFile || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; + + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } + + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async restoreBackup(file: File) { + try { + if (!file) { + throw new Error("No file uploaded"); + } + + if (!(file.name || "").endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } + + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return "Database restored successfully"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async addHost(host: DockerHost) { + try { + dbFunctions.addDockerHost(host); + return `Added docker host (${host.name} - ${host.hostAddress})`; + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateHost(host: DockerHost) { + try { + dbFunctions.updateDockerHost(host); + return `Updated docker host (${host.id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async removeHost(id: number) { + try { + dbFunctions.deleteDockerHost(id); + return `Deleted docker host (${id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } } export const ApiHandler = new apiHandler(); diff --git a/src/handlers/index.ts b/src/handlers/index.ts index d477548a..982f142b 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -9,15 +9,15 @@ import { ThemeHandler } from "./themes"; import { CheckHealth } from "./utils"; export const handlers = { - BasicDockerHandler, - ApiHandler, - DatabaseHandler, - StackHandler, - LogHandler, - CheckHealth, - Socket: "ws://localhost:4837/ws", - StoreHandler, - ThemeHandler, + BasicDockerHandler, + ApiHandler, + DatabaseHandler, + StackHandler, + LogHandler, + CheckHealth, + Socket: "ws://localhost:4837/ws", + StoreHandler, + ThemeHandler, }; Starter.startAll(); diff --git a/src/handlers/themes.ts b/src/handlers/themes.ts index 40677b57..ff646557 100644 --- a/src/handlers/themes.ts +++ b/src/handlers/themes.ts @@ -2,40 +2,40 @@ import { dbFunctions } from "~/core/database"; import type { Theme } from "~/typings/database"; class themeHandler { - getThemes(): Theme[] { - return dbFunctions.getThemes(); - } - addTheme(theme: Theme) { - try { - const rawVars = - typeof theme.vars === "string" ? JSON.parse(theme.vars) : theme.vars; + getThemes(): Theme[] { + return dbFunctions.getThemes(); + } + addTheme(theme: Theme) { + try { + const rawVars = + typeof theme.vars === "string" ? JSON.parse(theme.vars) : theme.vars; - const cssVars = Object.entries(rawVars) - .map(([key, value]) => `--${key}: ${value};`) - .join(" "); + const cssVars = Object.entries(rawVars) + .map(([key, value]) => `--${key}: ${value};`) + .join(" "); - const varsString = `.root, #root, #docs-root { ${cssVars} }`; + const varsString = `.root, #root, #docs-root { ${cssVars} }`; - return dbFunctions.addTheme({ - ...theme, - vars: varsString, - }); - } catch (error) { - throw new Error( - `Could not save theme ${JSON.stringify(theme)}, error: ${error}`, - ); - } - } - deleteTheme({ name }: Theme) { - try { - return dbFunctions.deleteTheme(name); - } catch (error) { - throw new Error(`Could not save theme ${name}, error: ${error}`); - } - } - getTheme(name: string): Theme { - return dbFunctions.getSpecificTheme(name); - } + return dbFunctions.addTheme({ + ...theme, + vars: varsString, + }); + } catch (error) { + throw new Error( + `Could not save theme ${JSON.stringify(theme)}, error: ${error}`, + ); + } + } + deleteTheme({ name }: Theme) { + try { + return dbFunctions.deleteTheme(name); + } catch (error) { + throw new Error(`Could not save theme ${name}, error: ${error}`); + } + } + getTheme(name: string): Theme { + return dbFunctions.getSpecificTheme(name); + } } export const ThemeHandler = new themeHandler(); diff --git a/src/handlers/utils.ts b/src/handlers/utils.ts index fd896dea..80b32d0b 100644 --- a/src/handlers/utils.ts +++ b/src/handlers/utils.ts @@ -1,6 +1,6 @@ import { logger } from "~/core/utils/logger"; -export async function CheckHealth() { +export async function CheckHealth(): Promise<"healthy"> { logger.info("Checking health"); return "healthy"; } From 5e0ede5031c26eeea35e14b86eb4b7e2a2f8dc00 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 12 Jul 2025 04:34:49 +0200 Subject: [PATCH 362/369] chore(config): Remove unused PackageJson import The `PackageJson` type from `knip` was imported but not used in the `config.ts` file. This commit removes the unused import to improve code clarity and reduce unnecessary dependencies. --- src/handlers/config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/handlers/config.ts b/src/handlers/config.ts index 1bd52d9d..3e33b55d 100644 --- a/src/handlers/config.ts +++ b/src/handlers/config.ts @@ -1,5 +1,4 @@ import { existsSync, readdirSync, unlinkSync } from "node:fs"; -import { PackageJson } from "knip/dist/types/package-json"; import { dbFunctions } from "~/core/database"; import { backupDir } from "~/core/database/backup"; import { pluginManager } from "~/core/plugins/plugin-manager"; From fe9fd190dc658cdec2d4b5a79da1c4f594662804 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sat, 12 Jul 2025 22:51:19 +0200 Subject: [PATCH 363/369] feat(database): Update default theme and init function - Updated the default theme variables in the database initialization to improve the initial user experience. - Minor formatting and consistency improvements in database initialization logic. --- src/core/database/database.ts | 134 ++++++++++++++++++---------------- 1 file changed, 70 insertions(+), 64 deletions(-) diff --git a/src/core/database/database.ts b/src/core/database/database.ts index 3b449ad2..f5949b41 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -13,26 +13,26 @@ const uid = userInfo().uid; export let db: Database; try { - const databasePath = path.join(dataFolder, "dockstatapi.db"); - console.log("Database path:", databasePath); - console.log(`Running as: ${username} (${uid}:${gid})`); + const databasePath = path.join(dataFolder, "dockstatapi.db"); + console.log("Database path:", databasePath); + console.log(`Running as: ${username} (${uid}:${gid})`); - if (!existsSync(dataFolder)) { - await mkdir(dataFolder, { recursive: true, mode: 0o777 }); - console.log("Created data directory:", dataFolder); - } + if (!existsSync(dataFolder)) { + await mkdir(dataFolder, { recursive: true, mode: 0o777 }); + console.log("Created data directory:", dataFolder); + } - db = new Database(databasePath, { create: true }); - console.log("Database opened successfully"); + db = new Database(databasePath, { create: true }); + console.log("Database opened successfully"); - db.exec("PRAGMA journal_mode = WAL;"); + db.exec("PRAGMA journal_mode = WAL;"); } catch (error) { - console.error(`Cannot start DockStatAPI: ${error}`); - process.exit(500); + console.error(`Cannot start DockStatAPI: ${error}`); + process.exit(500); } export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -105,62 +105,68 @@ export function init() { ) `); - const themeRows = db - .prepare("SELECT COUNT(*) AS count FROM themes") - .get() as { count: number }; + const themeRows = db + .prepare("SELECT COUNT(*) AS count FROM themes") + .get() as { count: number }; - const defaultCss = ` + const defaultCss = ` .root, #root, #docs-root { - --accent: #818cf8; - --secondary-accent: #a5b4fc; - --text-primary: #f3f4f6; - --text-secondary: #d1d5db; - --text-muted: #9ca3af; - --border: #4b5563; - --muted-bg: #18212f; - --gradient-from: #1f2937; - --gradient-to: #111827; + --accent: #818cf9; + --muted-bg: #0f172a; + --gradient-from: #1e293b; + --gradient-to: #334155; + --border: #334155; + --border-accent: rgba(129, 140, 249, 0.3); + --text-primary: #f8fafc; + --text-secondary: #94a3b8; + --text-tertiary: #64748b; + --state-success: #4ade80; + --state-warning: #facc15; + --state-error: #f87171; + --state-info: #38bdf8; + --shadow-glow: 0 0 15px rgba(129, 140, 249, 0.5); + --background-gradient: linear-gradient(145deg, #0f172a 0%, #1e293b 100%); } - `; - - if (themeRows.count === 0) { - db.prepare( - "INSERT INTO themes (name, creator, vars, tags) VALUES (?,?,?,?)", - ).run("default", "Its4Nik", defaultCss, "[default]"); - } - - const configRow = db - .prepare("SELECT COUNT(*) AS count FROM config") - .get() as { count: number }; - - if (configRow.count === 0) { - db.prepare( - "INSERT INTO config (keep_data_for, fetching_interval) VALUES (7, 5)", - ).run(); - } - - const hostRow = db - .prepare("SELECT COUNT(*) AS count FROM docker_hosts") - .get() as { count: number }; - - if (hostRow.count === 0) { - db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", - ).run("Localhost", "localhost:2375", false); - } - - const storeRow = db - .prepare("SELECT COUNT(*) AS count FROM store_repos") - .get() as { count: number }; - - if (storeRow.count === 0) { - db.prepare("INSERT INTO store_repos (slug, base) VALUES (?, ?)").run( - "DockStacks", - "https://raw.githubusercontent.com/Its4Nik/DockStacks/refs/heads/main/Index.json", - ); - } + `; + + if (themeRows.count === 0) { + db.prepare( + "INSERT INTO themes (name, creator, vars, tags) VALUES (?,?,?,?)", + ).run("default", "Its4Nik", defaultCss, "[default]"); + } + + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; + + if (configRow.count === 0) { + db.prepare( + "INSERT INTO config (keep_data_for, fetching_interval) VALUES (7, 5)", + ).run(); + } + + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; + + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ).run("Localhost", "localhost:2375", false); + } + + const storeRow = db + .prepare("SELECT COUNT(*) AS count FROM store_repos") + .get() as { count: number }; + + if (storeRow.count === 0) { + db.prepare("INSERT INTO store_repos (slug, base) VALUES (?, ?)").run( + "DockStacks", + "https://raw.githubusercontent.com/Its4Nik/DockStacks/refs/heads/main/Index.json", + ); + } } init(); From 253b0844e12d104529b4f6fe0605182a2df29e65 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 13 Jul 2025 14:17:08 +0200 Subject: [PATCH 364/369] feat(scheduler): Reload schedules on config update This commit introduces a mechanism to reload the schedules when the configuration is updated. This ensures that the scheduler is always running with the latest configuration settings. It also adds logging to the deletion of Themes The changes include: - Added a function to the file. This function clears all existing schedules and restarts them. - Modified the function in to call the function after updating the configuration in the database. - Added logging to theme deletion --- src/core/database/themes.ts | 28 +-- src/core/docker/scheduler.ts | 238 ++++++++++++---------- src/handlers/config.ts | 370 ++++++++++++++++++----------------- src/handlers/themes.ts | 63 +++--- typings | 2 +- 5 files changed, 366 insertions(+), 335 deletions(-) diff --git a/src/core/database/themes.ts b/src/core/database/themes.ts index 94dd42eb..08f245dd 100644 --- a/src/core/database/themes.ts +++ b/src/core/database/themes.ts @@ -1,32 +1,34 @@ import type { Theme } from "~/typings/database"; import { db } from "./database"; import { executeDbOperation } from "./helper"; +import { logger } from "../utils/logger"; const stmt = { - insert: db.prepare(` + insert: db.prepare(` INSERT INTO themes (name, creator, vars, tags) VALUES (?, ?, ?, ?) `), - remove: db.prepare("DELETE FROM themes WHERE name = ?"), - read: db.prepare("SELECT * FROM themes WHERE name = ?"), - readAll: db.prepare("SELECT * FROM themes"), + remove: db.prepare("DELETE FROM themes WHERE name = ?"), + read: db.prepare("SELECT * FROM themes WHERE name = ?"), + readAll: db.prepare("SELECT * FROM themes"), }; export function getThemes() { - return executeDbOperation("Get Themes", () => stmt.readAll.all()) as Theme[]; + return executeDbOperation("Get Themes", () => stmt.readAll.all()) as Theme[]; } export function addTheme({ name, creator, vars, tags }: Theme) { - return executeDbOperation("Save Theme", () => - stmt.insert.run(name, creator, vars, tags.toString()), - ); + return executeDbOperation("Save Theme", () => + stmt.insert.run(name, creator, vars, tags.toString()), + ); } export function getSpecificTheme(name: string): Theme { - return executeDbOperation( - "Getting specific Theme", - () => stmt.read.get(name) as Theme, - ); + return executeDbOperation( + "Getting specific Theme", + () => stmt.read.get(name) as Theme, + ); } export function deleteTheme(name: string) { - return executeDbOperation("Remove Theme", () => stmt.remove.run(name)); + logger.debug(`Removing ${name} from themes `); + return executeDbOperation("Remove Theme", () => stmt.remove.run(name)); } diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index 0ac78ad4..fa6da95c 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -5,120 +5,146 @@ import { logger } from "~/core/utils/logger"; import type { config } from "~/typings/database"; function convertFromMinToMs(minutes: number): number { - return minutes * 60 * 1000; + return minutes * 60 * 1000; } async function initialRun( - scheduleName: string, - scheduleFunction: Promise | void, - isAsync: boolean, + scheduleName: string, + scheduleFunction: Promise | void, + isAsync: boolean, ) { - try { - if (isAsync) { - await scheduleFunction; - } else { - scheduleFunction; - } - logger.info(`Startup run success for: ${scheduleName}`); - } catch (error) { - logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); - } + try { + if (isAsync) { + await scheduleFunction; + } else { + scheduleFunction; + } + logger.info(`Startup run success for: ${scheduleName}`); + } catch (error) { + logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); + } } -async function scheduledJob( - name: string, - jobFn: () => Promise, - intervalMs: number, -) { - while (true) { - const start = Date.now(); - logger.info(`Task Start: ${name}`); - try { - await jobFn(); - logger.info(`Task End: ${name} succeeded.`); - } catch (e) { - logger.error(`Task End: ${name} failed:`, e); - } - const elapsed = Date.now() - start; - const delay = Math.max(0, intervalMs - elapsed); - await new Promise((r) => setTimeout(r, delay)); - } +type CancelFn = () => void; +let cancelFunctions: CancelFn[] = []; + +async function reloadSchedules() { + logger.info("Reloading schedules..."); + + cancelFunctions.forEach((cancel) => cancel()); + cancelFunctions = []; + + await setSchedules(); +} + +function scheduledJob( + name: string, + jobFn: () => Promise, + intervalMs: number, +): CancelFn { + let stopped = false; + + async function run() { + if (stopped) return; + const start = Date.now(); + logger.info(`Task Start: ${name}`); + try { + await jobFn(); + logger.info(`Task End: ${name} succeeded.`); + } catch (e) { + logger.error(`Task End: ${name} failed:`, e); + } + const elapsed = Date.now() - start; + const delay = Math.max(0, intervalMs - elapsed); + setTimeout(run, delay); + } + + run(); + + return () => { + stopped = true; + }; } async function setSchedules() { - logger.info("Starting DockStatAPI"); - try { - const rawConfigData: unknown[] = dbFunctions.getConfig(); - const configData = rawConfigData[0]; - - if ( - !configData || - typeof (configData as config).keep_data_for !== "number" || - typeof (configData as config).fetching_interval !== "number" - ) { - logger.error("Invalid configuration data:", configData); - throw new Error("Invalid configuration data"); - } - - const { keep_data_for, fetching_interval } = configData as config; - - if (keep_data_for === undefined) { - const errMsg = "keep_data_for is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } - - if (fetching_interval === undefined) { - const errMsg = "fetching_interval is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } - - logger.info( - `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, - ); - - logger.info( - `Scheduling: Updating host statistics every ${fetching_interval} minutes`, - ); - - logger.info( - `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, - ); - - // Schedule container data fetching - await initialRun("storeContainerData", storeContainerData(), true); - scheduledJob( - "storeContainerData", - storeContainerData, - convertFromMinToMs(fetching_interval), - ); - - // Schedule Host statistics updates - await initialRun("storeHostData", storeHostData(), true); - scheduledJob( - "storeHostData", - storeHostData, - convertFromMinToMs(fetching_interval), - ); - - // Schedule database cleanup - await initialRun( - "dbFunctions.deleteOldData", - dbFunctions.deleteOldData(keep_data_for), - false, - ); - scheduledJob( - "cleanupOldData", - () => Promise.resolve(dbFunctions.deleteOldData(keep_data_for)), - convertFromMinToMs(60), - ); - - logger.info("Schedules have been set successfully."); - } catch (error) { - logger.error("Error setting schedules:", error); - throw new Error(error as string); - } + logger.info("Starting DockStatAPI"); + try { + const rawConfigData: unknown[] = dbFunctions.getConfig(); + const configData = rawConfigData[0]; + + if ( + !configData || + typeof (configData as config).keep_data_for !== "number" || + typeof (configData as config).fetching_interval !== "number" + ) { + logger.error("Invalid configuration data:", configData); + throw new Error("Invalid configuration data"); + } + + const { keep_data_for, fetching_interval } = configData as config; + + if (keep_data_for === undefined) { + const errMsg = "keep_data_for is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + if (fetching_interval === undefined) { + const errMsg = "fetching_interval is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + logger.info( + `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, + ); + + logger.info( + `Scheduling: Updating host statistics every ${fetching_interval} minutes`, + ); + + logger.info( + `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, + ); + // Schedule container data fetching + await initialRun("storeContainerData", storeContainerData(), true); + cancelFunctions.push( + scheduledJob( + "storeContainerData", + storeContainerData, + convertFromMinToMs(fetching_interval), + ), + ); + + // Schedule Host statistics updates + await initialRun("storeHostData", storeHostData(), true); + cancelFunctions.push( + scheduledJob( + "storeHostData", + storeHostData, + convertFromMinToMs(fetching_interval), + ), + ); + + // Schedule database cleanup + await initialRun( + "dbFunctions.deleteOldData", + dbFunctions.deleteOldData(keep_data_for), + false, + ); + cancelFunctions.push( + scheduledJob( + "cleanupOldData", + () => Promise.resolve(dbFunctions.deleteOldData(keep_data_for)), + convertFromMinToMs(60), + ), + ); + + logger.info("Schedules have been set successfully."); + } catch (error) { + logger.error("Error setting schedules:", error); + throw new Error(error as string); + } } -export { setSchedules }; +export { setSchedules, reloadSchedules }; diff --git a/src/handlers/config.ts b/src/handlers/config.ts index 3e33b55d..5f713f04 100644 --- a/src/handlers/config.ts +++ b/src/handlers/config.ts @@ -1,199 +1,201 @@ import { existsSync, readdirSync, unlinkSync } from "node:fs"; import { dbFunctions } from "~/core/database"; import { backupDir } from "~/core/database/backup"; +import { reloadSchedules } from "~/core/docker/scheduler"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import type { config } from "~/typings/database"; import type { DockerHost } from "~/typings/docker"; import type { PluginInfo } from "~/typings/plugin"; class apiHandler { - getConfig(): config { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateConfig(fetching_interval: number, keep_data_for: number) { - try { - logger.debug( - `Updated config: fetching_interval: ${fetching_interval} - keep_data_for: ${keep_data_for}`, - ); - dbFunctions.updateConfig(fetching_interval, keep_data_for); - return "Updated DockStatAPI config"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - getPlugins(): PluginInfo[] { - try { - logger.debug("Gathering plugins"); - return pluginManager.getPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - getPackage() { - try { - logger.debug("Fetching package.json"); - const data: { - version: string; - description: string; - license: string; - authorName: string; - authorEmail: string; - authorWebsite: string; - contributors: string[]; - dependencies: Record; - devDependencies: Record; - } = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json`, - ); - - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } - - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async createbackup(): Promise { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return backupFilename; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async listBackups() { - try { - const backupFiles = readdirSync(backupDir); - - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.startsWith(".") || - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); - - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async downloadbackup(downloadFile?: string) { - try { - const filename: string = downloadFile || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; - - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } - - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async restoreBackup(file: File) { - try { - if (!file) { - throw new Error("No file uploaded"); - } - - if (!(file.name || "").endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } - - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); - - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); - - return "Database restored successfully"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async addHost(host: DockerHost) { - try { - dbFunctions.addDockerHost(host); - return `Added docker host (${host.name} - ${host.hostAddress})`; - } catch (error: unknown) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateHost(host: DockerHost) { - try { - dbFunctions.updateDockerHost(host); - return `Updated docker host (${host.id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async removeHost(id: number) { - try { - dbFunctions.deleteDockerHost(id); - return `Deleted docker host (${id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } + getConfig(): config { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateConfig(fetching_interval: number, keep_data_for: number) { + try { + logger.debug( + `Updated config: fetching_interval: ${fetching_interval} - keep_data_for: ${keep_data_for}`, + ); + dbFunctions.updateConfig(fetching_interval, keep_data_for); + await reloadSchedules(); + return "Updated DockStatAPI config"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + getPlugins(): PluginInfo[] { + try { + logger.debug("Gathering plugins"); + return pluginManager.getPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + getPackage() { + try { + logger.debug("Fetching package.json"); + const data: { + version: string; + description: string; + license: string; + authorName: string; + authorEmail: string; + authorWebsite: string; + contributors: string[]; + dependencies: Record; + devDependencies: Record; + } = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, + ); + + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } + + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async createbackup(): Promise { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return backupFilename; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async listBackups() { + try { + const backupFiles = readdirSync(backupDir); + + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.startsWith(".") || + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); + + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async downloadbackup(downloadFile?: string) { + try { + const filename: string = downloadFile || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; + + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } + + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async restoreBackup(file: File) { + try { + if (!file) { + throw new Error("No file uploaded"); + } + + if (!(file.name || "").endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } + + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return "Database restored successfully"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async addHost(host: DockerHost) { + try { + dbFunctions.addDockerHost(host); + return `Added docker host (${host.name} - ${host.hostAddress})`; + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateHost(host: DockerHost) { + try { + dbFunctions.updateDockerHost(host); + return `Updated docker host (${host.id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async removeHost(id: number) { + try { + dbFunctions.deleteDockerHost(id); + return `Deleted docker host (${id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } } export const ApiHandler = new apiHandler(); diff --git a/src/handlers/themes.ts b/src/handlers/themes.ts index ff646557..8bc1f98d 100644 --- a/src/handlers/themes.ts +++ b/src/handlers/themes.ts @@ -2,40 +2,41 @@ import { dbFunctions } from "~/core/database"; import type { Theme } from "~/typings/database"; class themeHandler { - getThemes(): Theme[] { - return dbFunctions.getThemes(); - } - addTheme(theme: Theme) { - try { - const rawVars = - typeof theme.vars === "string" ? JSON.parse(theme.vars) : theme.vars; + getThemes(): Theme[] { + return dbFunctions.getThemes(); + } + addTheme(theme: Theme) { + try { + const rawVars = + typeof theme.vars === "string" ? JSON.parse(theme.vars) : theme.vars; - const cssVars = Object.entries(rawVars) - .map(([key, value]) => `--${key}: ${value};`) - .join(" "); + const cssVars = Object.entries(rawVars) + .map(([key, value]) => `--${key}: ${value};`) + .join(" "); - const varsString = `.root, #root, #docs-root { ${cssVars} }`; + const varsString = `.root, #root, #docs-root { ${cssVars} }`; - return dbFunctions.addTheme({ - ...theme, - vars: varsString, - }); - } catch (error) { - throw new Error( - `Could not save theme ${JSON.stringify(theme)}, error: ${error}`, - ); - } - } - deleteTheme({ name }: Theme) { - try { - return dbFunctions.deleteTheme(name); - } catch (error) { - throw new Error(`Could not save theme ${name}, error: ${error}`); - } - } - getTheme(name: string): Theme { - return dbFunctions.getSpecificTheme(name); - } + return dbFunctions.addTheme({ + ...theme, + vars: varsString, + }); + } catch (error) { + throw new Error( + `Could not save theme ${JSON.stringify(theme)}, error: ${error}`, + ); + } + } + deleteTheme(name: string) { + try { + dbFunctions.deleteTheme(name); + return "Deleted theme"; + } catch (error) { + throw new Error(`Could not save theme ${name}, error: ${error}`); + } + } + getTheme(name: string): Theme { + return dbFunctions.getSpecificTheme(name); + } } export const ThemeHandler = new themeHandler(); diff --git a/typings b/typings index e69df215..9d5500fc 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit e69df2154d40a533694ee810891fecfc88440ff9 +Subproject commit 9d5500fcbcb1d217b898ba85a929ebb26c42f898 From 42a5e94375a928d914e6e072f6881e79d49eb798 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 13 Jul 2025 12:17:49 +0000 Subject: [PATCH 365/369] Update dependency graphs --- dependency-graph.mmd | 132 +++--- dependency-graph.svg | 950 ++++++++++++++++++++++--------------------- 2 files changed, 549 insertions(+), 533 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 8108251e..60821b11 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -13,12 +13,12 @@ subgraph 2["handlers"] 4["config.ts"] subgraph P["modules"] Q["logs-socket.ts"] -1E["starter.ts"] -1F["docker-socket.ts"] +1I["starter.ts"] +1J["docker-socket.ts"] end -18["database.ts"] -19["docker.ts"] -1D["logs.ts"] +1F["database.ts"] +1G["docker.ts"] +1H["logs.ts"] 1L["stacks.ts"] 1U["store.ts"] 1V["themes.ts"] @@ -43,19 +43,19 @@ end subgraph N["utils"] O["logger.ts"] Y["helpers.ts"] -15["change-me-checker.ts"] -16["package-json.ts"] -1H["calculations.ts"] +15["calculations.ts"] +1C["change-me-checker.ts"] +1D["package-json.ts"] end -subgraph 11["plugins"] -12["plugin-manager.ts"] -14["loader.ts"] +subgraph 11["docker"] +12["scheduler.ts"] +13["store-container-stats.ts"] +14["client.ts"] +16["store-host-stats.ts"] end -subgraph 1B["docker"] -1C["client.ts"] -1I["scheduler.ts"] -1J["store-container-stats.ts"] -1K["store-host-stats.ts"] +subgraph 18["plugins"] +19["plugin-manager.ts"] +1B["loader.ts"] end subgraph 1M["stacks"] 1N["controller.ts"] @@ -74,8 +74,8 @@ subgraph 6["typings"] 8["docker"] 9["plugin"] F["misc"] -1A["dockerode"] -1G["websocket"] +17["dockerode"] +1K["websocket"] 1O["docker-compose"] end end @@ -86,14 +86,14 @@ I["bun:sqlite"] K["os"] L["path"] R["stream"] -13["events"] -17["package.json"] +1A["events"] +1E["package.json"] 1-->3 3-->4 -3-->18 -3-->19 -3-->1D -3-->1E +3-->1F +3-->1G +3-->1H +3-->1I 3-->1L 3-->1U 3-->1V @@ -101,8 +101,9 @@ R["stream"] 4-->D 4-->E 4-->12 +4-->19 4-->O -4-->16 +4-->1D 4-->7 4-->8 4-->9 @@ -159,63 +160,64 @@ X-->7 Y-->O Z-->H Z-->M +10-->O 10-->H 10-->M 10-->7 -12-->O -12-->14 -12-->8 -12-->9 +12-->D 12-->13 -14-->15 +12-->16 +12-->O +12-->7 +13-->O +13-->D +13-->14 +13-->15 +13-->7 14-->O -14-->12 -14-->A -14-->L -15-->O -15-->J +14-->8 +16-->D +16-->14 +16-->O +16-->8 16-->17 -18-->D -19-->D -19-->1C 19-->O +19-->1B 19-->8 +19-->9 19-->1A +1B-->1C +1B-->O +1B-->19 +1B-->A +1B-->L 1C-->O -1C-->8 -1D-->D -1D-->O -1E-->1F -1E-->1I -1E-->12 -1F-->Q +1C-->J +1D-->1E 1F-->D -1F-->1C -1F-->1H -1F-->O -1F-->7 -1F-->8 -1F-->1G -1I-->D +1G-->D +1G-->14 +1G-->O +1G-->8 +1G-->17 +1H-->D +1H-->O 1I-->1J -1I-->1K -1I-->O -1I-->7 -1J-->O +1I-->12 +1I-->19 +1J-->Q 1J-->D -1J-->1C -1J-->1H +1J-->14 +1J-->15 +1J-->O 1J-->7 -1K-->D -1K-->1C -1K-->O -1K-->8 -1K-->1A +1J-->8 +1J-->1K 1L-->D 1L-->1N 1L-->O 1L-->7 -1N-->1F +1N-->1J 1N-->1P 1N-->1R 1N-->1S @@ -227,7 +229,7 @@ Z-->M 1N-->J 1P-->D 1P-->O -1R-->1F +1R-->1J 1R-->1S 1R-->O 1R-->1O diff --git a/dependency-graph.svg b/dependency-graph.svg index 84e6375e..495b4a21 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -31,8 +31,8 @@ cluster_src/core/docker - -docker + +docker cluster_src/core/plugins @@ -41,13 +41,13 @@ cluster_src/core/stacks - -stacks + +stacks cluster_src/core/stacks/operations - -operations + +operations cluster_src/core/utils @@ -141,8 +141,8 @@ src/core/database/_dbState.ts - -_dbState.ts + +_dbState.ts @@ -150,22 +150,22 @@ src/core/database/backup.ts - -backup.ts + +backup.ts src/core/database/backup.ts->fs - - + + src/core/database/backup.ts->src/core/database/_dbState.ts - - + + @@ -179,8 +179,8 @@ src/core/database/backup.ts->src/core/database/database.ts - - + + @@ -194,27 +194,27 @@ src/core/database/backup.ts->src/core/database/helper.ts - - - - + + + + src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + @@ -228,7 +228,7 @@ src/core/database/backup.ts->~/typings/misc - + @@ -246,14 +246,14 @@ src/core/database/database.ts->fs/promises - - + + src/core/database/database.ts->os - - + + @@ -264,28 +264,28 @@ src/core/database/helper.ts->src/core/database/_dbState.ts - - + + src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + - + src/core/utils/logger.ts->path - + - + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + @@ -297,10 +297,10 @@ - + src/core/utils/logger.ts->~/typings/database - - + + @@ -312,12 +312,12 @@ - + src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + @@ -329,12 +329,12 @@ - + src/core/utils/logger.ts->src/handlers/modules/logs-socket.ts - - - - + + + + @@ -348,68 +348,68 @@ src/core/database/config.ts->src/core/database/database.ts - - + + src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts->~/typings/database - - + + src/core/database/dockerHosts.ts - -dockerHosts.ts + +dockerHosts.ts src/core/database/dockerHosts.ts->src/core/database/database.ts - - + + src/core/database/dockerHosts.ts->src/core/database/helper.ts - - - - + + + + @@ -423,83 +423,83 @@ src/core/database/dockerHosts.ts->~/typings/docker - - + + src/core/database/hostStats.ts - -hostStats.ts + +hostStats.ts src/core/database/hostStats.ts->src/core/database/database.ts - - + + src/core/database/hostStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/hostStats.ts->~/typings/docker - - + + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + src/core/database/index.ts->src/core/database/dockerHosts.ts - - - - + + + + src/core/database/index.ts->src/core/database/hostStats.ts - - - - + + + + @@ -513,101 +513,101 @@ src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + src/core/database/stacks.ts - -stacks.ts + +stacks.ts src/core/database/index.ts->src/core/database/stacks.ts - - - - + + + + src/core/database/stores.ts - -stores.ts + +stores.ts src/core/database/index.ts->src/core/database/stores.ts - - - - + + + + src/core/database/themes.ts - -themes.ts + +themes.ts src/core/database/index.ts->src/core/database/themes.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + src/core/database/logs.ts->~/typings/database - - + + src/core/database/stacks.ts->src/core/database/database.ts - - + + src/core/database/stacks.ts->src/core/database/helper.ts - - - - + + + + src/core/database/stacks.ts->~/typings/database - - + + @@ -621,152 +621,160 @@ src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - + + + + src/core/database/stores.ts->src/core/database/database.ts - - + + src/core/database/stores.ts->src/core/database/helper.ts - - - - + + + + - + src/core/database/themes.ts->src/core/database/database.ts - - + + - + src/core/database/themes.ts->src/core/database/helper.ts - - - - + + + + + + + +src/core/database/themes.ts->src/core/utils/logger.ts + + + + - + src/core/database/themes.ts->~/typings/database - - + + - + src/core/utils/helpers.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts - + src/core/docker/client.ts->src/core/utils/logger.ts - - + + - + src/core/docker/client.ts->~/typings/docker - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts - + src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + - + src/core/docker/scheduler.ts->~/typings/database - - + + - + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts - + src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-container-stats.ts->~/typings/database - - + + - + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + @@ -778,34 +786,34 @@ - + src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + - + src/core/docker/store-host-stats.ts->~/typings/docker - + - + src/core/docker/store-host-stats.ts->src/core/database/index.ts - - + + - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + @@ -817,10 +825,10 @@ - + src/core/docker/store-host-stats.ts->~/typings/dockerode - - + + @@ -832,22 +840,22 @@ - + src/core/plugins/loader.ts->fs - - + + - + src/core/plugins/loader.ts->path - - + + - + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + @@ -859,10 +867,10 @@ - + src/core/plugins/loader.ts->src/core/utils/change-me-checker.ts - - + + @@ -874,50 +882,50 @@ - + src/core/plugins/loader.ts->src/core/plugins/plugin-manager.ts - - - - + + + + - + src/core/utils/change-me-checker.ts->fs/promises - + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->events - + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + - + src/core/plugins/plugin-manager.ts->~/typings/docker - - + + - + src/core/plugins/plugin-manager.ts->src/core/plugins/loader.ts - - - - + + + + @@ -929,7 +937,7 @@ - + src/core/plugins/plugin-manager.ts->~/typings/plugin @@ -938,61 +946,61 @@ src/core/stacks/checker.ts - -checker.ts + +checker.ts - + src/core/stacks/checker.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/checker.ts->src/core/database/index.ts - - + + src/core/stacks/controller.ts - -controller.ts + +controller.ts - + src/core/stacks/controller.ts->fs/promises - - + + - + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/controller.ts->~/typings/database - - + + - + src/core/stacks/controller.ts->src/core/database/index.ts - - + + - + src/core/stacks/controller.ts->src/core/stacks/checker.ts - - + + @@ -1004,55 +1012,55 @@ - + src/core/stacks/controller.ts->src/handlers/modules/docker-socket.ts - - + + src/core/stacks/operations/runStackCommand.ts - -runStackCommand.ts + +runStackCommand.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/runStackCommand.ts - - + + src/core/stacks/operations/stackHelpers.ts - -stackHelpers.ts + +stackHelpers.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/stackHelpers.ts - - + + src/core/stacks/operations/stackStatus.ts - -stackStatus.ts + +stackStatus.ts - + src/core/stacks/controller.ts->src/core/stacks/operations/stackStatus.ts - - + + @@ -1064,49 +1072,49 @@ - + src/core/stacks/controller.ts->~/typings/docker-compose - - + + - + src/handlers/modules/docker-socket.ts->src/core/utils/logger.ts - - + + - + src/handlers/modules/docker-socket.ts->~/typings/database - - + + - + src/handlers/modules/docker-socket.ts->~/typings/docker - + src/handlers/modules/docker-socket.ts->src/core/database/index.ts - - + + - + src/handlers/modules/docker-socket.ts->src/core/docker/client.ts - - + + - + src/handlers/modules/docker-socket.ts->src/core/utils/calculations.ts - - + + - + src/handlers/modules/docker-socket.ts->src/handlers/modules/logs-socket.ts @@ -1121,90 +1129,90 @@ - + src/handlers/modules/docker-socket.ts->~/typings/websocket - + src/core/stacks/operations/runStackCommand.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/operations/runStackCommand.ts->src/handlers/modules/docker-socket.ts - - + + - + src/core/stacks/operations/runStackCommand.ts->src/core/stacks/operations/stackHelpers.ts - - + + - + src/core/stacks/operations/runStackCommand.ts->~/typings/docker-compose - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/database/index.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->src/core/utils/helpers.ts - - + + - + src/core/stacks/operations/stackHelpers.ts->~/typings/docker-compose - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/utils/logger.ts - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/database/index.ts - - + + - + src/core/stacks/operations/stackStatus.ts->src/core/stacks/operations/runStackCommand.ts - - + + - + src/handlers/modules/logs-socket.ts->src/core/utils/logger.ts - - - - + + + + - + src/handlers/modules/logs-socket.ts->~/typings/database - - + + @@ -1216,10 +1224,10 @@ - + src/handlers/modules/logs-socket.ts->stream - - + + @@ -1231,7 +1239,7 @@ - + src/core/utils/package-json.ts->package.json @@ -1246,58 +1254,64 @@ - + src/handlers/config.ts->fs - + src/handlers/config.ts->src/core/database/backup.ts - - + + - + src/handlers/config.ts->src/core/utils/logger.ts - - + + - + src/handlers/config.ts->~/typings/database - - + + - + src/handlers/config.ts->~/typings/docker - - + + - + src/handlers/config.ts->src/core/database/index.ts - - + + + + + +src/handlers/config.ts->src/core/docker/scheduler.ts + + - + src/handlers/config.ts->src/core/plugins/plugin-manager.ts - - + + - + src/handlers/config.ts->~/typings/plugin - + src/handlers/config.ts->src/core/utils/package-json.ts - - + + @@ -1309,10 +1323,10 @@ - + src/handlers/database.ts->src/core/database/index.ts - - + + @@ -1324,31 +1338,31 @@ - + src/handlers/docker.ts->src/core/utils/logger.ts - - + + - + src/handlers/docker.ts->~/typings/docker - - + + - + src/handlers/docker.ts->src/core/database/index.ts - - + + - + src/handlers/docker.ts->src/core/docker/client.ts - - + + - + src/handlers/docker.ts->~/typings/dockerode @@ -1363,19 +1377,19 @@ - + src/handlers/index.ts->src/handlers/config.ts - - + + - + src/handlers/index.ts->src/handlers/database.ts - + src/handlers/index.ts->src/handlers/docker.ts @@ -1390,7 +1404,7 @@ - + src/handlers/index.ts->src/handlers/logs.ts @@ -1405,10 +1419,10 @@ - + src/handlers/index.ts->src/handlers/modules/starter.ts - - + + @@ -1420,7 +1434,7 @@ - + src/handlers/index.ts->src/handlers/stacks.ts @@ -1435,7 +1449,7 @@ - + src/handlers/index.ts->src/handlers/store.ts @@ -1450,7 +1464,7 @@ - + src/handlers/index.ts->src/handlers/themes.ts @@ -1465,88 +1479,88 @@ - + src/handlers/index.ts->src/handlers/utils.ts - + src/handlers/logs.ts->src/core/utils/logger.ts - - + + - + src/handlers/logs.ts->src/core/database/index.ts - - + + - + src/handlers/modules/starter.ts->src/core/docker/scheduler.ts - - + + - + src/handlers/modules/starter.ts->src/core/plugins/plugin-manager.ts - - + + - + src/handlers/modules/starter.ts->src/handlers/modules/docker-socket.ts - - + + - + src/handlers/stacks.ts->src/core/utils/logger.ts - - + + - + src/handlers/stacks.ts->~/typings/database - - + + - + src/handlers/stacks.ts->src/core/database/index.ts - - + + - + src/handlers/stacks.ts->src/core/stacks/controller.ts - - + + - + src/handlers/store.ts->src/core/database/stores.ts - - + + - + src/handlers/themes.ts->~/typings/database - - + + - + src/handlers/themes.ts->src/core/database/index.ts - - + + - + src/handlers/utils.ts->src/core/utils/logger.ts - - + + @@ -1558,7 +1572,7 @@ - + src/index.ts->src/handlers/index.ts From 830c36dd9e9fa36202f9855491b0ee2eadce1e86 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 17 Jul 2025 23:16:53 +0200 Subject: [PATCH 366/369] =?UTF-8?q?=E2=9C=A8=20Feat:=20Add=20container=20n?= =?UTF-8?q?etwork=20stats=20and=20improve=20data=20fetching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces the collection of network statistics for Docker containers, including receive and transmit rates, and total bytes. It also refactors and improves the data fetching and processing logic for container stats, including adding timestamps and improving validation. - Add `network_rx_rate`, `network_tx_rate`, `network_rx_bytes`, and `network_tx_bytes` columns to the `container_stats` table. - Update the `addContainerStats` function to include network stats and a timestamp. - Add a new function `getLastContainerStats` to retrieve the last recorded stats for a container, used for calculating rates. - Refactor `storeContainerData` to fetch and calculate network stats, including using the previous stats to calculate RX/TX rates. - Improve data validation for container stats. - Update default theme colors to be more accessible. --- docker/docker-compose.dev.yaml | 6 +- src/core/database/containerStats.ts | 108 +++++-- src/core/database/database.ts | 12 +- src/core/database/themes.ts | 30 +- src/core/docker/scheduler.ts | 242 +++++++-------- src/core/docker/store-container-stats.ts | 147 ++++----- src/core/utils/calculations.ts | 72 +++-- src/handlers/config.ts | 370 +++++++++++------------ src/handlers/database.ts | 14 +- src/handlers/themes.ts | 64 ++-- 10 files changed, 565 insertions(+), 500 deletions(-) diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index 7d4e6ca8..b654f55c 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -5,7 +5,7 @@ services: image: lscr.io/linuxserver/socket-proxy:latest volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - restart: never + restart: no read_only: true tmpfs: - /run @@ -44,10 +44,10 @@ services: sqlite-web: container_name: sqlite-web image: ghcr.io/coleifer/sqlite-web:latest - restart: never + restart: no ports: - 8080:8080 volumes: - - /home/nik/Documents/Code-local/dockstat-project/DockStat/data:/data:ro + - /home/nik/Documents/Code-local/dockstat-project/DockStat/server/data:/data:ro environment: - SQLITE_DATABASE=dockstatapi.db diff --git a/src/core/database/containerStats.ts b/src/core/database/containerStats.ts index a8466701..c301646d 100644 --- a/src/core/database/containerStats.ts +++ b/src/core/database/containerStats.ts @@ -3,41 +3,89 @@ import { db } from "./database"; import { executeDbOperation } from "./helper"; const insert = db.prepare(` - INSERT INTO container_stats (id, hostId, name, image, status, state, cpu_usage, memory_usage) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO container_stats ( + id, + hostId, + name, + image, + status, + state, + cpu_usage, + memory_usage, + network_rx_rate, + network_tx_rate, + network_rx_bytes, + network_tx_bytes, + timestamp + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); -const get = db.prepare("SELECT * FROM container_stats"); +const getOne = db.prepare(` + SELECT * + FROM container_stats + WHERE hostId = ? + AND id = ? +ORDER BY timestamp DESC + LIMIT 1 +`); + +const getAll = db.prepare(` + SELECT * + FROM container_stats +`); export function addContainerStats(stats: container_stats) { - return executeDbOperation( - "Add Container Stats", - () => - insert.run( - stats.id, - stats.hostId, - stats.name, - stats.image, - stats.status, - stats.state, - stats.cpu_usage, - stats.memory_usage, - ), - () => { - if ( - typeof stats.id !== "string" || - typeof stats.hostId !== "number" || - typeof stats.cpu_usage !== "number" || - typeof stats.memory_usage !== "number" - ) { - throw new TypeError("Invalid container stats parameters"); - } - }, - ); + return executeDbOperation( + "Add Container Stats", + () => + insert.run( + stats.id, + stats.hostId, + stats.name, + stats.image, + stats.status, + stats.state, + stats.cpu_usage, + stats.memory_usage || 0, + stats.network_rx_rate, + stats.network_tx_rate, + stats.network_rx_bytes, + stats.network_tx_bytes, + stats.timestamp || new Date().toISOString(), + ), + () => { + if ( + typeof stats.id !== "string" || + typeof stats.hostId !== "number" || + typeof stats.name !== "string" || + typeof stats.image !== "string" || + typeof stats.status !== "string" || + typeof stats.state !== "string" || + typeof stats.cpu_usage !== "number" || + typeof stats.memory_usage !== "number" || + typeof stats.network_rx_rate !== "number" || + typeof stats.network_tx_rate !== "number" || + typeof stats.network_rx_bytes !== "number" || + typeof stats.network_tx_bytes !== "number" + ) { + throw new TypeError("Invalid container stats parameters"); + } + }, + ); } export function getContainerStats(): container_stats[] { - return executeDbOperation("Get Container Stats", () => - get.all(), - ) as container_stats[]; + return executeDbOperation("Get All Container Stats", () => + getAll.all(), + ) as container_stats[]; +} + +export function getLastContainerStats( + hostId: number, + containerId: string, +): container_stats | undefined { + return executeDbOperation( + "Get Last Container Stat", + () => getOne.get(hostId, containerId) as container_stats, + ); } diff --git a/src/core/database/database.ts b/src/core/database/database.ts index f5949b41..febd33cb 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -84,7 +84,11 @@ export function init() { status TEXT NOT NULL, state TEXT NOT NULL, cpu_usage FLOAT NOT NULL, - memory_usage, + memory_usage FLOAT NOT NULL, + network_rx_rate NUMBER NOT NULL, + network_tx_rate NUMBER NOT NULL, + network_rx_bytes NUMBER NOT NULL, + network_tx_bytes NUMBER NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP ); @@ -118,7 +122,7 @@ export function init() { --gradient-from: #1e293b; --gradient-to: #334155; --border: #334155; - --border-accent: rgba(129, 140, 249, 0.3); + --border-accent: #818cf94d; --text-primary: #f8fafc; --text-secondary: #94a3b8; --text-tertiary: #64748b; @@ -126,7 +130,7 @@ export function init() { --state-warning: #facc15; --state-error: #f87171; --state-info: #38bdf8; - --shadow-glow: 0 0 15px rgba(129, 140, 249, 0.5); + --shadow-glow: 0 0 15px #818cf980; --background-gradient: linear-gradient(145deg, #0f172a 0%, #1e293b 100%); } `; @@ -134,7 +138,7 @@ export function init() { if (themeRows.count === 0) { db.prepare( "INSERT INTO themes (name, creator, vars, tags) VALUES (?,?,?,?)", - ).run("default", "Its4Nik", defaultCss, "[default]"); + ).run("default", "Its4Nik", defaultCss, "default, dark"); } const configRow = db diff --git a/src/core/database/themes.ts b/src/core/database/themes.ts index 08f245dd..141d8719 100644 --- a/src/core/database/themes.ts +++ b/src/core/database/themes.ts @@ -1,34 +1,34 @@ import type { Theme } from "~/typings/database"; +import { logger } from "../utils/logger"; import { db } from "./database"; import { executeDbOperation } from "./helper"; -import { logger } from "../utils/logger"; const stmt = { - insert: db.prepare(` + insert: db.prepare(` INSERT INTO themes (name, creator, vars, tags) VALUES (?, ?, ?, ?) `), - remove: db.prepare("DELETE FROM themes WHERE name = ?"), - read: db.prepare("SELECT * FROM themes WHERE name = ?"), - readAll: db.prepare("SELECT * FROM themes"), + remove: db.prepare("DELETE FROM themes WHERE name = ?"), + read: db.prepare("SELECT * FROM themes WHERE name = ?"), + readAll: db.prepare("SELECT * FROM themes"), }; export function getThemes() { - return executeDbOperation("Get Themes", () => stmt.readAll.all()) as Theme[]; + return executeDbOperation("Get Themes", () => stmt.readAll.all()) as Theme[]; } export function addTheme({ name, creator, vars, tags }: Theme) { - return executeDbOperation("Save Theme", () => - stmt.insert.run(name, creator, vars, tags.toString()), - ); + return executeDbOperation("Save Theme", () => + stmt.insert.run(name, creator, vars, tags.toString()), + ); } export function getSpecificTheme(name: string): Theme { - return executeDbOperation( - "Getting specific Theme", - () => stmt.read.get(name) as Theme, - ); + return executeDbOperation( + "Getting specific Theme", + () => stmt.read.get(name) as Theme, + ); } export function deleteTheme(name: string) { - logger.debug(`Removing ${name} from themes `); - return executeDbOperation("Remove Theme", () => stmt.remove.run(name)); + logger.debug(`Removing ${name} from themes `); + return executeDbOperation("Remove Theme", () => stmt.remove.run(name)); } diff --git a/src/core/docker/scheduler.ts b/src/core/docker/scheduler.ts index fa6da95c..fd6571e5 100644 --- a/src/core/docker/scheduler.ts +++ b/src/core/docker/scheduler.ts @@ -5,146 +5,146 @@ import { logger } from "~/core/utils/logger"; import type { config } from "~/typings/database"; function convertFromMinToMs(minutes: number): number { - return minutes * 60 * 1000; + return minutes * 60 * 1000; } async function initialRun( - scheduleName: string, - scheduleFunction: Promise | void, - isAsync: boolean, + scheduleName: string, + scheduleFunction: Promise | void, + isAsync: boolean, ) { - try { - if (isAsync) { - await scheduleFunction; - } else { - scheduleFunction; - } - logger.info(`Startup run success for: ${scheduleName}`); - } catch (error) { - logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); - } + try { + if (isAsync) { + await scheduleFunction; + } else { + scheduleFunction; + } + logger.info(`Startup run success for: ${scheduleName}`); + } catch (error) { + logger.error(`Startup run failed for ${scheduleName}, ${error as string}`); + } } type CancelFn = () => void; let cancelFunctions: CancelFn[] = []; async function reloadSchedules() { - logger.info("Reloading schedules..."); + logger.info("Reloading schedules..."); - cancelFunctions.forEach((cancel) => cancel()); - cancelFunctions = []; + cancelFunctions.forEach((cancel) => cancel()); + cancelFunctions = []; - await setSchedules(); + await setSchedules(); } function scheduledJob( - name: string, - jobFn: () => Promise, - intervalMs: number, + name: string, + jobFn: () => Promise, + intervalMs: number, ): CancelFn { - let stopped = false; - - async function run() { - if (stopped) return; - const start = Date.now(); - logger.info(`Task Start: ${name}`); - try { - await jobFn(); - logger.info(`Task End: ${name} succeeded.`); - } catch (e) { - logger.error(`Task End: ${name} failed:`, e); - } - const elapsed = Date.now() - start; - const delay = Math.max(0, intervalMs - elapsed); - setTimeout(run, delay); - } - - run(); - - return () => { - stopped = true; - }; + let stopped = false; + + async function run() { + if (stopped) return; + const start = Date.now(); + logger.info(`Task Start: ${name}`); + try { + await jobFn(); + logger.info(`Task End: ${name} succeeded.`); + } catch (e) { + logger.error(`Task End: ${name} failed:`, e); + } + const elapsed = Date.now() - start; + const delay = Math.max(0, intervalMs - elapsed); + setTimeout(run, delay); + } + + run(); + + return () => { + stopped = true; + }; } async function setSchedules() { - logger.info("Starting DockStatAPI"); - try { - const rawConfigData: unknown[] = dbFunctions.getConfig(); - const configData = rawConfigData[0]; - - if ( - !configData || - typeof (configData as config).keep_data_for !== "number" || - typeof (configData as config).fetching_interval !== "number" - ) { - logger.error("Invalid configuration data:", configData); - throw new Error("Invalid configuration data"); - } - - const { keep_data_for, fetching_interval } = configData as config; - - if (keep_data_for === undefined) { - const errMsg = "keep_data_for is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } - - if (fetching_interval === undefined) { - const errMsg = "fetching_interval is undefined"; - logger.error(errMsg); - throw new Error(errMsg); - } - - logger.info( - `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, - ); - - logger.info( - `Scheduling: Updating host statistics every ${fetching_interval} minutes`, - ); - - logger.info( - `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, - ); - // Schedule container data fetching - await initialRun("storeContainerData", storeContainerData(), true); - cancelFunctions.push( - scheduledJob( - "storeContainerData", - storeContainerData, - convertFromMinToMs(fetching_interval), - ), - ); - - // Schedule Host statistics updates - await initialRun("storeHostData", storeHostData(), true); - cancelFunctions.push( - scheduledJob( - "storeHostData", - storeHostData, - convertFromMinToMs(fetching_interval), - ), - ); - - // Schedule database cleanup - await initialRun( - "dbFunctions.deleteOldData", - dbFunctions.deleteOldData(keep_data_for), - false, - ); - cancelFunctions.push( - scheduledJob( - "cleanupOldData", - () => Promise.resolve(dbFunctions.deleteOldData(keep_data_for)), - convertFromMinToMs(60), - ), - ); - - logger.info("Schedules have been set successfully."); - } catch (error) { - logger.error("Error setting schedules:", error); - throw new Error(error as string); - } + logger.info("Starting DockStatAPI"); + try { + const rawConfigData: unknown[] = dbFunctions.getConfig(); + const configData = rawConfigData[0]; + + if ( + !configData || + typeof (configData as config).keep_data_for !== "number" || + typeof (configData as config).fetching_interval !== "number" + ) { + logger.error("Invalid configuration data:", configData); + throw new Error("Invalid configuration data"); + } + + const { keep_data_for, fetching_interval } = configData as config; + + if (keep_data_for === undefined) { + const errMsg = "keep_data_for is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + if (fetching_interval === undefined) { + const errMsg = "fetching_interval is undefined"; + logger.error(errMsg); + throw new Error(errMsg); + } + + logger.info( + `Scheduling: Fetching container statistics every ${fetching_interval} minutes`, + ); + + logger.info( + `Scheduling: Updating host statistics every ${fetching_interval} minutes`, + ); + + logger.info( + `Scheduling: Cleaning up Database every hour and deleting data older then ${keep_data_for} days`, + ); + // Schedule container data fetching + await initialRun("storeContainerData", storeContainerData(), true); + cancelFunctions.push( + scheduledJob( + "storeContainerData", + storeContainerData, + convertFromMinToMs(fetching_interval), + ), + ); + + // Schedule Host statistics updates + await initialRun("storeHostData", storeHostData(), true); + cancelFunctions.push( + scheduledJob( + "storeHostData", + storeHostData, + convertFromMinToMs(fetching_interval), + ), + ); + + // Schedule database cleanup + await initialRun( + "dbFunctions.deleteOldData", + dbFunctions.deleteOldData(keep_data_for), + false, + ); + cancelFunctions.push( + scheduledJob( + "cleanupOldData", + () => Promise.resolve(dbFunctions.deleteOldData(keep_data_for)), + convertFromMinToMs(60), + ), + ); + + logger.info("Schedules have been set successfully."); + } catch (error) { + logger.error("Error setting schedules:", error); + throw new Error(error as string); + } } export { setSchedules, reloadSchedules }; diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts index a2778777..18b6c1c8 100644 --- a/src/core/docker/store-container-stats.ts +++ b/src/core/docker/store-container-stats.ts @@ -2,100 +2,83 @@ import type Docker from "dockerode"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, + calculateCpuPercent, + calculateMemoryUsage, + sumNetworkBytes, + calcRate, } from "~/core/utils/calculations"; import type { container_stats } from "~/typings/database"; import { logger } from "../utils/logger"; async function storeContainerData() { - try { - const hosts = dbFunctions.getDockerHosts(); - logger.debug("Retrieved docker hosts for storing container data"); + const hosts = dbFunctions.getDockerHosts(); + logger.debug("Retrieved docker hosts"); - // Process each host concurrently and wait for them all to finish - await Promise.all( - hosts.map(async (host) => { - const docker = getDockerClient(host); + await Promise.all( + hosts.map(async (host) => { + const docker = getDockerClient(host); + await docker.ping(); - // Test the connection with a ping - try { - await docker.ping(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to ping docker host "${host.name}": ${errMsg}`, - ); - } + const containers = await docker.listContainers({ all: true }); + await Promise.all( + containers.map(async (info) => { + const container = docker.getContainer(info.Id); + const rawStats: Docker.ContainerStats = await new Promise( + (res, rej) => + container.stats({ stream: false }, (err, stats) => + err ? rej(err) : res(stats as Docker.ContainerStats), + ), + ); - let containers: Docker.ContainerInfo[] = []; - try { - containers = await docker.listContainers({ all: true }); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error( - `Failed to list containers on host "${host.name}": ${errMsg}`, - ); - } + const now = new Date(); - // Process each container concurrently - await Promise.all( - containers.map(async (containerInfo) => { - const containerName = containerInfo.Names[0].replace(/^\//, ""); - try { - const container = docker.getContainer(containerInfo.Id); + const cpu_usage = calculateCpuPercent(rawStats); + const memory_usage = calculateMemoryUsage(rawStats); - const stats: Docker.ContainerStats = await new Promise( - (resolve, reject) => { - container.stats({ stream: false }, (error, stats) => { - if (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - return reject( - new Error( - `Failed to get stats for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, - ), - ); - } - if (!stats) { - return reject( - new Error( - `No stats returned for container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}".`, - ), - ); - } - resolve(stats); - }); - }, - ); + const { rx: network_rx_bytes, tx: network_tx_bytes } = + sumNetworkBytes(rawStats); - const parsed: container_stats = { - cpu_usage: calculateCpuPercent(stats), - hostId: host.id, - id: containerInfo.Id, - image: containerInfo.Image, - memory_usage: calculateMemoryUsage(stats), - name: containerName, - state: containerInfo.State, - status: containerInfo.Status, - }; + const prev = dbFunctions.getLastContainerStats(host.id, info.Id); - dbFunctions.addContainerStats(parsed); - } catch (error) { - const errMsg = - error instanceof Error ? error.message : String(error); - throw new Error( - `Error processing container "${containerName}" (ID: ${containerInfo.Id}) on host "${host.name}": ${errMsg}`, - ); - } - }), - ); - }), - ); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to store container data: ${errMsg}`); - } + let network_rx_rate: number | null = null; + let network_tx_rate: number | null = null; + if (prev) { + logger.debug(`Loaded previous data: ${JSON.stringify(prev)}`); + network_rx_rate = calcRate( + prev.network_rx_bytes, + prev.timestamp || new Date().toISOString(), + network_rx_bytes, + now, + ); + network_tx_rate = calcRate( + prev.network_tx_bytes, + prev.timestamp || new Date().toISOString(), + network_tx_bytes, + now, + ); + } + + const parsed: container_stats = { + id: info.Id, + hostId: host.id, + name: info.Names[0].replace(/^\//, ""), + image: info.Image, + state: info.State, + status: info.Status, + cpu_usage, + memory_usage, + network_rx_bytes, + network_tx_bytes, + network_rx_rate: network_rx_rate || 0, + network_tx_rate: network_tx_rate || 0, + timestamp: now.toISOString(), + }; + + dbFunctions.addContainerStats(parsed); + }), + ); + }), + ); } export default storeContainerData; diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index fbb7a422..6919adb6 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -1,37 +1,65 @@ import type Docker from "dockerode"; const calculateCpuPercent = (stats: Docker.ContainerStats): number => { - const cpuDelta = - stats.cpu_stats.cpu_usage.total_usage - - stats.precpu_stats.cpu_usage.total_usage; - const systemDelta = - stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + const cpuDelta = + stats.cpu_stats.cpu_usage.total_usage - + stats.precpu_stats.cpu_usage.total_usage; + const systemDelta = + stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; - if (cpuDelta <= 0) { - return 0.0000001; - } + if (cpuDelta <= 0) { + return 0.0000001; + } - if (systemDelta <= 0) { - return 0.0000001; - } + if (systemDelta <= 0) { + return 0.0000001; + } - const data = (cpuDelta / systemDelta) * 100; + const data = (cpuDelta / systemDelta) * 100; - if (data === null) { - return 0.0000001; - } + if (data === null) { + return 0.0000001; + } - return data * 10; + return data * 10; }; const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { - if (stats.memory_stats.usage === null) { - return 0.0000001; - } + if (stats.memory_stats.usage === null) { + return 0.0000001; + } - const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; + const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; - return data; + return data; }; -export { calculateCpuPercent, calculateMemoryUsage }; +function sumNetworkBytes(stats: Docker.ContainerStats) { + const nets = stats.networks ?? {}; + return Object.values(nets).reduce( + (acc, iface) => { + acc.rx += iface.rx_bytes; + acc.tx += iface.tx_bytes; + return acc; + }, + { rx: 0, tx: 0 }, + ); +} + +/** + * Given current and previous cumulative bytes + timestamps, + * compute bytes/sec. If no previous sample, return 0. + */ +function calcRate( + prevBytes: number, + prevTime: string, + currBytes: number, + currTime: Date, +): number { + const dateConst = new Date(prevTime); + const deltaSec = (currTime.getTime() - dateConst.getTime()) / 1000; + if (deltaSec <= 0) return 0; + return (currBytes - prevBytes) / deltaSec; +} + +export { calculateCpuPercent, calculateMemoryUsage, sumNetworkBytes, calcRate }; diff --git a/src/handlers/config.ts b/src/handlers/config.ts index 5f713f04..13131d38 100644 --- a/src/handlers/config.ts +++ b/src/handlers/config.ts @@ -5,197 +5,197 @@ import { reloadSchedules } from "~/core/docker/scheduler"; import { pluginManager } from "~/core/plugins/plugin-manager"; import { logger } from "~/core/utils/logger"; import { - authorEmail, - authorName, - authorWebsite, - contributors, - dependencies, - description, - devDependencies, - license, - version, + authorEmail, + authorName, + authorWebsite, + contributors, + dependencies, + description, + devDependencies, + license, + version, } from "~/core/utils/package-json"; import type { config } from "~/typings/database"; import type { DockerHost } from "~/typings/docker"; import type { PluginInfo } from "~/typings/plugin"; class apiHandler { - getConfig(): config { - try { - const data = dbFunctions.getConfig() as config[]; - const distinct = data[0]; - - logger.debug("Fetched backend config"); - return distinct; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateConfig(fetching_interval: number, keep_data_for: number) { - try { - logger.debug( - `Updated config: fetching_interval: ${fetching_interval} - keep_data_for: ${keep_data_for}`, - ); - dbFunctions.updateConfig(fetching_interval, keep_data_for); - await reloadSchedules(); - return "Updated DockStatAPI config"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - getPlugins(): PluginInfo[] { - try { - logger.debug("Gathering plugins"); - return pluginManager.getPlugins(); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - getPackage() { - try { - logger.debug("Fetching package.json"); - const data: { - version: string; - description: string; - license: string; - authorName: string; - authorEmail: string; - authorWebsite: string; - contributors: string[]; - dependencies: Record; - devDependencies: Record; - } = { - version: version, - description: description, - license: license, - authorName: authorName, - authorEmail: authorEmail, - authorWebsite: authorWebsite, - contributors: contributors, - dependencies: dependencies, - devDependencies: devDependencies, - }; - - logger.debug( - `Received: ${JSON.stringify(data).length} chars in package.json`, - ); - - if (JSON.stringify(data).length <= 10) { - throw new Error("Failed to read package.json"); - } - - return data; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async createbackup(): Promise { - try { - const backupFilename = await dbFunctions.backupDatabase(); - return backupFilename; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async listBackups() { - try { - const backupFiles = readdirSync(backupDir); - - const filteredFiles = backupFiles.filter((file: string) => { - return !( - file.startsWith(".") || - file.endsWith(".db") || - file.endsWith(".db-shm") || - file.endsWith(".db-wal") - ); - }); - - return filteredFiles; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async downloadbackup(downloadFile?: string) { - try { - const filename: string = downloadFile || dbFunctions.findLatestBackup(); - const filePath = `${backupDir}/${filename}`; - - if (!existsSync(filePath)) { - throw new Error("Backup file not found"); - } - - return Bun.file(filePath); - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async restoreBackup(file: File) { - try { - if (!file) { - throw new Error("No file uploaded"); - } - - if (!(file.name || "").endsWith(".db.bak")) { - throw new Error("Invalid file type. Expected .db.bak"); - } - - const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; - const fileBuffer = await file.arrayBuffer(); - - await Bun.write(tempPath, fileBuffer); - dbFunctions.restoreDatabase(tempPath); - unlinkSync(tempPath); - - return "Database restored successfully"; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async addHost(host: DockerHost) { - try { - dbFunctions.addDockerHost(host); - return `Added docker host (${host.name} - ${host.hostAddress})`; - } catch (error: unknown) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async updateHost(host: DockerHost) { - try { - dbFunctions.updateDockerHost(host); - return `Updated docker host (${host.id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } - - async removeHost(id: number) { - try { - dbFunctions.deleteDockerHost(id); - return `Deleted docker host (${id})`; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - throw new Error(errMsg); - } - } + getConfig(): config { + try { + const data = dbFunctions.getConfig() as config[]; + const distinct = data[0]; + + logger.debug("Fetched backend config"); + return distinct; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateConfig(fetching_interval: number, keep_data_for: number) { + try { + logger.debug( + `Updated config: fetching_interval: ${fetching_interval} - keep_data_for: ${keep_data_for}`, + ); + dbFunctions.updateConfig(fetching_interval, keep_data_for); + await reloadSchedules(); + return "Updated DockStatAPI config"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + getPlugins(): PluginInfo[] { + try { + logger.debug("Gathering plugins"); + return pluginManager.getPlugins(); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + getPackage() { + try { + logger.debug("Fetching package.json"); + const data: { + version: string; + description: string; + license: string; + authorName: string; + authorEmail: string; + authorWebsite: string; + contributors: string[]; + dependencies: Record; + devDependencies: Record; + } = { + version: version, + description: description, + license: license, + authorName: authorName, + authorEmail: authorEmail, + authorWebsite: authorWebsite, + contributors: contributors, + dependencies: dependencies, + devDependencies: devDependencies, + }; + + logger.debug( + `Received: ${JSON.stringify(data).length} chars in package.json`, + ); + + if (JSON.stringify(data).length <= 10) { + throw new Error("Failed to read package.json"); + } + + return data; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async createbackup(): Promise { + try { + const backupFilename = await dbFunctions.backupDatabase(); + return backupFilename; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async listBackups() { + try { + const backupFiles = readdirSync(backupDir); + + const filteredFiles = backupFiles.filter((file: string) => { + return !( + file.startsWith(".") || + file.endsWith(".db") || + file.endsWith(".db-shm") || + file.endsWith(".db-wal") + ); + }); + + return filteredFiles; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async downloadbackup(downloadFile?: string) { + try { + const filename: string = downloadFile || dbFunctions.findLatestBackup(); + const filePath = `${backupDir}/${filename}`; + + if (!existsSync(filePath)) { + throw new Error("Backup file not found"); + } + + return Bun.file(filePath); + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async restoreBackup(file: File) { + try { + if (!file) { + throw new Error("No file uploaded"); + } + + if (!(file.name || "").endsWith(".db.bak")) { + throw new Error("Invalid file type. Expected .db.bak"); + } + + const tempPath = `${backupDir}/upload_${Date.now()}.db.bak`; + const fileBuffer = await file.arrayBuffer(); + + await Bun.write(tempPath, fileBuffer); + dbFunctions.restoreDatabase(tempPath); + unlinkSync(tempPath); + + return "Database restored successfully"; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async addHost(host: DockerHost) { + try { + dbFunctions.addDockerHost(host); + return `Added docker host (${host.name} - ${host.hostAddress})`; + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async updateHost(host: DockerHost) { + try { + dbFunctions.updateDockerHost(host); + return `Updated docker host (${host.id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } + + async removeHost(id: number) { + try { + dbFunctions.deleteDockerHost(id); + return `Deleted docker host (${id})`; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + throw new Error(errMsg); + } + } } export const ApiHandler = new apiHandler(); diff --git a/src/handlers/database.ts b/src/handlers/database.ts index d6bd0c49..76f4b4c3 100644 --- a/src/handlers/database.ts +++ b/src/handlers/database.ts @@ -1,13 +1,15 @@ import { dbFunctions } from "~/core/database"; +import type { container_stats } from "~/typings/database"; +import type { HostStats } from "~/typings/docker"; class databaseHandler { - async getContainers() { - return dbFunctions.getContainerStats(); - } + getContainers(): container_stats[] { + return dbFunctions.getContainerStats(); + } - async getHosts() { - return dbFunctions.getHostStats(); - } + getHosts(): HostStats[] { + return dbFunctions.getHostStats(); + } } export const DatabaseHandler = new databaseHandler(); diff --git a/src/handlers/themes.ts b/src/handlers/themes.ts index 8bc1f98d..c915d1b2 100644 --- a/src/handlers/themes.ts +++ b/src/handlers/themes.ts @@ -2,41 +2,41 @@ import { dbFunctions } from "~/core/database"; import type { Theme } from "~/typings/database"; class themeHandler { - getThemes(): Theme[] { - return dbFunctions.getThemes(); - } - addTheme(theme: Theme) { - try { - const rawVars = - typeof theme.vars === "string" ? JSON.parse(theme.vars) : theme.vars; + getThemes(): Theme[] { + return dbFunctions.getThemes(); + } + addTheme(theme: Theme) { + try { + const rawVars = + typeof theme.vars === "string" ? JSON.parse(theme.vars) : theme.vars; - const cssVars = Object.entries(rawVars) - .map(([key, value]) => `--${key}: ${value};`) - .join(" "); + const cssVars = Object.entries(rawVars) + .map(([key, value]) => `--${key}: ${value};`) + .join(" "); - const varsString = `.root, #root, #docs-root { ${cssVars} }`; + const varsString = `.root, #root, #docs-root { ${cssVars} }`; - return dbFunctions.addTheme({ - ...theme, - vars: varsString, - }); - } catch (error) { - throw new Error( - `Could not save theme ${JSON.stringify(theme)}, error: ${error}`, - ); - } - } - deleteTheme(name: string) { - try { - dbFunctions.deleteTheme(name); - return "Deleted theme"; - } catch (error) { - throw new Error(`Could not save theme ${name}, error: ${error}`); - } - } - getTheme(name: string): Theme { - return dbFunctions.getSpecificTheme(name); - } + return dbFunctions.addTheme({ + ...theme, + vars: varsString, + }); + } catch (error) { + throw new Error( + `Could not save theme ${JSON.stringify(theme)}, error: ${error}`, + ); + } + } + deleteTheme(name: string) { + try { + dbFunctions.deleteTheme(name); + return "Deleted theme"; + } catch (error) { + throw new Error(`Could not save theme ${name}, error: ${error}`); + } + } + getTheme(name: string): Theme { + return dbFunctions.getSpecificTheme(name); + } } export const ThemeHandler = new themeHandler(); From 15a6d01c7868906a9cb7354a60892bea4baa9d83 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 25 Jul 2025 00:34:43 +0200 Subject: [PATCH 367/369] =?UTF-8?q?=E2=9C=A8=20Feat:=20Add=20theme=20optio?= =?UTF-8?q?ns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces the ability to store and retrieve theme options in the database. The `themes` table in the database has been updated to include an `options` column, which stores the theme options as a JSON string. The `addTheme` function now accepts an `options` parameter and stores it in the database. A new function `getThemeOptions` has been added to retrieve the theme options for a given theme. This change allows for more flexible and customizable themes. --- src/core/database/database.ts | 18 ++++++++- src/core/database/themes.ts | 55 +++++++++++++++++--------- src/handlers/themes.ts | 72 +++++++++++++++++++---------------- 3 files changed, 93 insertions(+), 52 deletions(-) diff --git a/src/core/database/database.ts b/src/core/database/database.ts index febd33cb..982f2738 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -105,6 +105,7 @@ export function init() { name TEXT PRIMARY KEY, creator TEXT NOT NULL, vars TEXT NOT NULL, + options TEXT NOT NULL, tags TEXT NOT NULL ) `); @@ -135,10 +136,23 @@ export function init() { } `; + const defaultThemeOptions = { + backgroundAnimation: { + enabled: true, + from: ["#c084fc", "#818cf9", "#60a5fa"], + }, + }; + if (themeRows.count === 0) { db.prepare( - "INSERT INTO themes (name, creator, vars, tags) VALUES (?,?,?,?)", - ).run("default", "Its4Nik", defaultCss, "default, dark"); + "INSERT INTO themes (name, creator, vars, options, tags) VALUES (?,?,?,?,?)", + ).run( + "default", + "Its4Nik", + defaultCss, + JSON.stringify(defaultThemeOptions), + "default, dark", + ); } const configRow = db diff --git a/src/core/database/themes.ts b/src/core/database/themes.ts index 141d8719..8d0f346b 100644 --- a/src/core/database/themes.ts +++ b/src/core/database/themes.ts @@ -1,34 +1,55 @@ -import type { Theme } from "~/typings/database"; +import type { Theme, ThemeOptions } from "~/typings/database"; import { logger } from "../utils/logger"; import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare(` - INSERT INTO themes (name, creator, vars, tags) VALUES (?, ?, ?, ?) + insert: db.prepare(` + INSERT INTO themes (name, creator, vars, options, tags) VALUES (?, ?, ?, ?, ?) `), - remove: db.prepare("DELETE FROM themes WHERE name = ?"), - read: db.prepare("SELECT * FROM themes WHERE name = ?"), - readAll: db.prepare("SELECT * FROM themes"), + remove: db.prepare("DELETE FROM themes WHERE name = ?"), + read: db.prepare( + "SELECT name, creator, vars, tags FROM themes WHERE name = ?", + ), + readOptions: db.prepare("SELECT options FROM themes WHERE name = ?"), + readAll: db.prepare("SELECT * FROM themes"), }; export function getThemes() { - return executeDbOperation("Get Themes", () => stmt.readAll.all()) as Theme[]; + return executeDbOperation("Get Themes", () => stmt.readAll.all()) as Theme[]; } -export function addTheme({ name, creator, vars, tags }: Theme) { - return executeDbOperation("Save Theme", () => - stmt.insert.run(name, creator, vars, tags.toString()), - ); +export function addTheme({ name, creator, options, vars, tags }: Theme) { + return executeDbOperation("Save Theme", () => + stmt.insert.run( + name, + creator, + vars, + JSON.stringify(options), + tags.toString(), + ), + ); } + export function getSpecificTheme(name: string): Theme { - return executeDbOperation( - "Getting specific Theme", - () => stmt.read.get(name) as Theme, - ); + return executeDbOperation( + "Getting specific Theme", + () => stmt.read.get(name) as Theme, + ); +} + +export function getThemeOptions(name: string): ThemeOptions { + const data = executeDbOperation( + "Getting Theme Options", + () => (stmt.readOptions.get(name) as { options: string }).options, + ); + + logger.debug(`RAW DB: ${JSON.stringify(stmt.readOptions.get(name))}`); + + return JSON.parse(data); } export function deleteTheme(name: string) { - logger.debug(`Removing ${name} from themes `); - return executeDbOperation("Remove Theme", () => stmt.remove.run(name)); + logger.debug(`Removing ${name} from themes `); + return executeDbOperation("Remove Theme", () => stmt.remove.run(name)); } diff --git a/src/handlers/themes.ts b/src/handlers/themes.ts index c915d1b2..db4a5131 100644 --- a/src/handlers/themes.ts +++ b/src/handlers/themes.ts @@ -1,42 +1,48 @@ import { dbFunctions } from "~/core/database"; -import type { Theme } from "~/typings/database"; +import { logger } from "~/core/utils/logger"; +import type { Theme, ThemeOptions } from "~/typings/database"; class themeHandler { - getThemes(): Theme[] { - return dbFunctions.getThemes(); - } - addTheme(theme: Theme) { - try { - const rawVars = - typeof theme.vars === "string" ? JSON.parse(theme.vars) : theme.vars; + getThemes(): Theme[] { + return dbFunctions.getThemes(); + } + addTheme(theme: Theme) { + try { + const rawVars = + typeof theme.vars === "string" ? JSON.parse(theme.vars) : theme.vars; - const cssVars = Object.entries(rawVars) - .map(([key, value]) => `--${key}: ${value};`) - .join(" "); + const cssVars = Object.entries(rawVars) + .map(([key, value]) => `--${key}: ${value};`) + .join(" "); - const varsString = `.root, #root, #docs-root { ${cssVars} }`; + const varsString = `.root, #root, #docs-root { ${cssVars} }`; - return dbFunctions.addTheme({ - ...theme, - vars: varsString, - }); - } catch (error) { - throw new Error( - `Could not save theme ${JSON.stringify(theme)}, error: ${error}`, - ); - } - } - deleteTheme(name: string) { - try { - dbFunctions.deleteTheme(name); - return "Deleted theme"; - } catch (error) { - throw new Error(`Could not save theme ${name}, error: ${error}`); - } - } - getTheme(name: string): Theme { - return dbFunctions.getSpecificTheme(name); - } + return dbFunctions.addTheme({ + ...theme, + vars: varsString, + }); + } catch (error) { + throw new Error( + `Could not save theme ${JSON.stringify(theme)}, error: ${error}`, + ); + } + } + deleteTheme(name: string) { + try { + dbFunctions.deleteTheme(name); + return "Deleted theme"; + } catch (error) { + throw new Error(`Could not save theme ${name}, error: ${error}`); + } + } + getTheme(name: string): Theme { + return dbFunctions.getSpecificTheme(name); + } + getThemeOptions(name: string): ThemeOptions { + const data = dbFunctions.getThemeOptions(name); + logger.debug(`Received ${JSON.stringify(data)}`); + return data; + } } export const ThemeHandler = new themeHandler(); From efb28a0973526701cecbee93df7488da873c6f5e Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Fri, 25 Jul 2025 00:35:18 +0200 Subject: [PATCH 368/369] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20Apply?= =?UTF-8?q?=20formatting=20and=20consistency=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit applies consistent formatting and removes unnecessary blank lines across several files, improving code readability and maintainability. No functional changes are included. --- src/core/database/containerStats.ts | 92 ++++++++-------- src/core/database/database.ts | 132 +++++++++++------------ src/core/database/themes.ts | 58 +++++----- src/core/docker/store-container-stats.ts | 126 +++++++++++----------- src/core/utils/calculations.ts | 76 ++++++------- src/handlers/database.ts | 12 +-- src/handlers/themes.ts | 74 ++++++------- 7 files changed, 285 insertions(+), 285 deletions(-) diff --git a/src/core/database/containerStats.ts b/src/core/database/containerStats.ts index c301646d..6e7eede5 100644 --- a/src/core/database/containerStats.ts +++ b/src/core/database/containerStats.ts @@ -35,57 +35,57 @@ const getAll = db.prepare(` `); export function addContainerStats(stats: container_stats) { - return executeDbOperation( - "Add Container Stats", - () => - insert.run( - stats.id, - stats.hostId, - stats.name, - stats.image, - stats.status, - stats.state, - stats.cpu_usage, - stats.memory_usage || 0, - stats.network_rx_rate, - stats.network_tx_rate, - stats.network_rx_bytes, - stats.network_tx_bytes, - stats.timestamp || new Date().toISOString(), - ), - () => { - if ( - typeof stats.id !== "string" || - typeof stats.hostId !== "number" || - typeof stats.name !== "string" || - typeof stats.image !== "string" || - typeof stats.status !== "string" || - typeof stats.state !== "string" || - typeof stats.cpu_usage !== "number" || - typeof stats.memory_usage !== "number" || - typeof stats.network_rx_rate !== "number" || - typeof stats.network_tx_rate !== "number" || - typeof stats.network_rx_bytes !== "number" || - typeof stats.network_tx_bytes !== "number" - ) { - throw new TypeError("Invalid container stats parameters"); - } - }, - ); + return executeDbOperation( + "Add Container Stats", + () => + insert.run( + stats.id, + stats.hostId, + stats.name, + stats.image, + stats.status, + stats.state, + stats.cpu_usage, + stats.memory_usage || 0, + stats.network_rx_rate, + stats.network_tx_rate, + stats.network_rx_bytes, + stats.network_tx_bytes, + stats.timestamp || new Date().toISOString(), + ), + () => { + if ( + typeof stats.id !== "string" || + typeof stats.hostId !== "number" || + typeof stats.name !== "string" || + typeof stats.image !== "string" || + typeof stats.status !== "string" || + typeof stats.state !== "string" || + typeof stats.cpu_usage !== "number" || + typeof stats.memory_usage !== "number" || + typeof stats.network_rx_rate !== "number" || + typeof stats.network_tx_rate !== "number" || + typeof stats.network_rx_bytes !== "number" || + typeof stats.network_tx_bytes !== "number" + ) { + throw new TypeError("Invalid container stats parameters"); + } + }, + ); } export function getContainerStats(): container_stats[] { - return executeDbOperation("Get All Container Stats", () => - getAll.all(), - ) as container_stats[]; + return executeDbOperation("Get All Container Stats", () => + getAll.all(), + ) as container_stats[]; } export function getLastContainerStats( - hostId: number, - containerId: string, + hostId: number, + containerId: string, ): container_stats | undefined { - return executeDbOperation( - "Get Last Container Stat", - () => getOne.get(hostId, containerId) as container_stats, - ); + return executeDbOperation( + "Get Last Container Stat", + () => getOne.get(hostId, containerId) as container_stats, + ); } diff --git a/src/core/database/database.ts b/src/core/database/database.ts index 982f2738..6d5d6d35 100644 --- a/src/core/database/database.ts +++ b/src/core/database/database.ts @@ -13,26 +13,26 @@ const uid = userInfo().uid; export let db: Database; try { - const databasePath = path.join(dataFolder, "dockstatapi.db"); - console.log("Database path:", databasePath); - console.log(`Running as: ${username} (${uid}:${gid})`); + const databasePath = path.join(dataFolder, "dockstatapi.db"); + console.log("Database path:", databasePath); + console.log(`Running as: ${username} (${uid}:${gid})`); - if (!existsSync(dataFolder)) { - await mkdir(dataFolder, { recursive: true, mode: 0o777 }); - console.log("Created data directory:", dataFolder); - } + if (!existsSync(dataFolder)) { + await mkdir(dataFolder, { recursive: true, mode: 0o777 }); + console.log("Created data directory:", dataFolder); + } - db = new Database(databasePath, { create: true }); - console.log("Database opened successfully"); + db = new Database(databasePath, { create: true }); + console.log("Database opened successfully"); - db.exec("PRAGMA journal_mode = WAL;"); + db.exec("PRAGMA journal_mode = WAL;"); } catch (error) { - console.error(`Cannot start DockStatAPI: ${error}`); - process.exit(500); + console.error(`Cannot start DockStatAPI: ${error}`); + process.exit(500); } export function init() { - db.exec(` + db.exec(` CREATE TABLE IF NOT EXISTS backend_log_entries ( timestamp STRING NOT NULL, level TEXT NOT NULL, @@ -110,11 +110,11 @@ export function init() { ) `); - const themeRows = db - .prepare("SELECT COUNT(*) AS count FROM themes") - .get() as { count: number }; + const themeRows = db + .prepare("SELECT COUNT(*) AS count FROM themes") + .get() as { count: number }; - const defaultCss = ` + const defaultCss = ` .root, #root, #docs-root { @@ -136,55 +136,55 @@ export function init() { } `; - const defaultThemeOptions = { - backgroundAnimation: { - enabled: true, - from: ["#c084fc", "#818cf9", "#60a5fa"], - }, - }; - - if (themeRows.count === 0) { - db.prepare( - "INSERT INTO themes (name, creator, vars, options, tags) VALUES (?,?,?,?,?)", - ).run( - "default", - "Its4Nik", - defaultCss, - JSON.stringify(defaultThemeOptions), - "default, dark", - ); - } - - const configRow = db - .prepare("SELECT COUNT(*) AS count FROM config") - .get() as { count: number }; - - if (configRow.count === 0) { - db.prepare( - "INSERT INTO config (keep_data_for, fetching_interval) VALUES (7, 5)", - ).run(); - } - - const hostRow = db - .prepare("SELECT COUNT(*) AS count FROM docker_hosts") - .get() as { count: number }; - - if (hostRow.count === 0) { - db.prepare( - "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", - ).run("Localhost", "localhost:2375", false); - } - - const storeRow = db - .prepare("SELECT COUNT(*) AS count FROM store_repos") - .get() as { count: number }; - - if (storeRow.count === 0) { - db.prepare("INSERT INTO store_repos (slug, base) VALUES (?, ?)").run( - "DockStacks", - "https://raw.githubusercontent.com/Its4Nik/DockStacks/refs/heads/main/Index.json", - ); - } + const defaultThemeOptions = { + backgroundAnimation: { + enabled: true, + from: ["#c084fc", "#818cf9", "#60a5fa"], + }, + }; + + if (themeRows.count === 0) { + db.prepare( + "INSERT INTO themes (name, creator, vars, options, tags) VALUES (?,?,?,?,?)", + ).run( + "default", + "Its4Nik", + defaultCss, + JSON.stringify(defaultThemeOptions), + "default, dark", + ); + } + + const configRow = db + .prepare("SELECT COUNT(*) AS count FROM config") + .get() as { count: number }; + + if (configRow.count === 0) { + db.prepare( + "INSERT INTO config (keep_data_for, fetching_interval) VALUES (7, 5)", + ).run(); + } + + const hostRow = db + .prepare("SELECT COUNT(*) AS count FROM docker_hosts") + .get() as { count: number }; + + if (hostRow.count === 0) { + db.prepare( + "INSERT INTO docker_hosts (name, hostAddress, secure) VALUES (?, ?, ?)", + ).run("Localhost", "localhost:2375", false); + } + + const storeRow = db + .prepare("SELECT COUNT(*) AS count FROM store_repos") + .get() as { count: number }; + + if (storeRow.count === 0) { + db.prepare("INSERT INTO store_repos (slug, base) VALUES (?, ?)").run( + "DockStacks", + "https://raw.githubusercontent.com/Its4Nik/DockStacks/refs/heads/main/Index.json", + ); + } } init(); diff --git a/src/core/database/themes.ts b/src/core/database/themes.ts index 8d0f346b..b4836f8f 100644 --- a/src/core/database/themes.ts +++ b/src/core/database/themes.ts @@ -4,52 +4,52 @@ import { db } from "./database"; import { executeDbOperation } from "./helper"; const stmt = { - insert: db.prepare(` + insert: db.prepare(` INSERT INTO themes (name, creator, vars, options, tags) VALUES (?, ?, ?, ?, ?) `), - remove: db.prepare("DELETE FROM themes WHERE name = ?"), - read: db.prepare( - "SELECT name, creator, vars, tags FROM themes WHERE name = ?", - ), - readOptions: db.prepare("SELECT options FROM themes WHERE name = ?"), - readAll: db.prepare("SELECT * FROM themes"), + remove: db.prepare("DELETE FROM themes WHERE name = ?"), + read: db.prepare( + "SELECT name, creator, vars, tags FROM themes WHERE name = ?", + ), + readOptions: db.prepare("SELECT options FROM themes WHERE name = ?"), + readAll: db.prepare("SELECT * FROM themes"), }; export function getThemes() { - return executeDbOperation("Get Themes", () => stmt.readAll.all()) as Theme[]; + return executeDbOperation("Get Themes", () => stmt.readAll.all()) as Theme[]; } export function addTheme({ name, creator, options, vars, tags }: Theme) { - return executeDbOperation("Save Theme", () => - stmt.insert.run( - name, - creator, - vars, - JSON.stringify(options), - tags.toString(), - ), - ); + return executeDbOperation("Save Theme", () => + stmt.insert.run( + name, + creator, + vars, + JSON.stringify(options), + tags.toString(), + ), + ); } export function getSpecificTheme(name: string): Theme { - return executeDbOperation( - "Getting specific Theme", - () => stmt.read.get(name) as Theme, - ); + return executeDbOperation( + "Getting specific Theme", + () => stmt.read.get(name) as Theme, + ); } export function getThemeOptions(name: string): ThemeOptions { - const data = executeDbOperation( - "Getting Theme Options", - () => (stmt.readOptions.get(name) as { options: string }).options, - ); + const data = executeDbOperation( + "Getting Theme Options", + () => (stmt.readOptions.get(name) as { options: string }).options, + ); - logger.debug(`RAW DB: ${JSON.stringify(stmt.readOptions.get(name))}`); + logger.debug(`RAW DB: ${JSON.stringify(stmt.readOptions.get(name))}`); - return JSON.parse(data); + return JSON.parse(data); } export function deleteTheme(name: string) { - logger.debug(`Removing ${name} from themes `); - return executeDbOperation("Remove Theme", () => stmt.remove.run(name)); + logger.debug(`Removing ${name} from themes `); + return executeDbOperation("Remove Theme", () => stmt.remove.run(name)); } diff --git a/src/core/docker/store-container-stats.ts b/src/core/docker/store-container-stats.ts index 18b6c1c8..fa3e2ad9 100644 --- a/src/core/docker/store-container-stats.ts +++ b/src/core/docker/store-container-stats.ts @@ -2,83 +2,83 @@ import type Docker from "dockerode"; import { dbFunctions } from "~/core/database"; import { getDockerClient } from "~/core/docker/client"; import { - calculateCpuPercent, - calculateMemoryUsage, - sumNetworkBytes, - calcRate, + calcRate, + calculateCpuPercent, + calculateMemoryUsage, + sumNetworkBytes, } from "~/core/utils/calculations"; import type { container_stats } from "~/typings/database"; import { logger } from "../utils/logger"; async function storeContainerData() { - const hosts = dbFunctions.getDockerHosts(); - logger.debug("Retrieved docker hosts"); + const hosts = dbFunctions.getDockerHosts(); + logger.debug("Retrieved docker hosts"); - await Promise.all( - hosts.map(async (host) => { - const docker = getDockerClient(host); - await docker.ping(); + await Promise.all( + hosts.map(async (host) => { + const docker = getDockerClient(host); + await docker.ping(); - const containers = await docker.listContainers({ all: true }); - await Promise.all( - containers.map(async (info) => { - const container = docker.getContainer(info.Id); - const rawStats: Docker.ContainerStats = await new Promise( - (res, rej) => - container.stats({ stream: false }, (err, stats) => - err ? rej(err) : res(stats as Docker.ContainerStats), - ), - ); + const containers = await docker.listContainers({ all: true }); + await Promise.all( + containers.map(async (info) => { + const container = docker.getContainer(info.Id); + const rawStats: Docker.ContainerStats = await new Promise( + (res, rej) => + container.stats({ stream: false }, (err, stats) => + err ? rej(err) : res(stats as Docker.ContainerStats), + ), + ); - const now = new Date(); + const now = new Date(); - const cpu_usage = calculateCpuPercent(rawStats); - const memory_usage = calculateMemoryUsage(rawStats); + const cpu_usage = calculateCpuPercent(rawStats); + const memory_usage = calculateMemoryUsage(rawStats); - const { rx: network_rx_bytes, tx: network_tx_bytes } = - sumNetworkBytes(rawStats); + const { rx: network_rx_bytes, tx: network_tx_bytes } = + sumNetworkBytes(rawStats); - const prev = dbFunctions.getLastContainerStats(host.id, info.Id); + const prev = dbFunctions.getLastContainerStats(host.id, info.Id); - let network_rx_rate: number | null = null; - let network_tx_rate: number | null = null; - if (prev) { - logger.debug(`Loaded previous data: ${JSON.stringify(prev)}`); - network_rx_rate = calcRate( - prev.network_rx_bytes, - prev.timestamp || new Date().toISOString(), - network_rx_bytes, - now, - ); - network_tx_rate = calcRate( - prev.network_tx_bytes, - prev.timestamp || new Date().toISOString(), - network_tx_bytes, - now, - ); - } + let network_rx_rate: number | null = null; + let network_tx_rate: number | null = null; + if (prev) { + logger.debug(`Loaded previous data: ${JSON.stringify(prev)}`); + network_rx_rate = calcRate( + prev.network_rx_bytes, + prev.timestamp || new Date().toISOString(), + network_rx_bytes, + now, + ); + network_tx_rate = calcRate( + prev.network_tx_bytes, + prev.timestamp || new Date().toISOString(), + network_tx_bytes, + now, + ); + } - const parsed: container_stats = { - id: info.Id, - hostId: host.id, - name: info.Names[0].replace(/^\//, ""), - image: info.Image, - state: info.State, - status: info.Status, - cpu_usage, - memory_usage, - network_rx_bytes, - network_tx_bytes, - network_rx_rate: network_rx_rate || 0, - network_tx_rate: network_tx_rate || 0, - timestamp: now.toISOString(), - }; + const parsed: container_stats = { + id: info.Id, + hostId: host.id, + name: info.Names[0].replace(/^\//, ""), + image: info.Image, + state: info.State, + status: info.Status, + cpu_usage, + memory_usage, + network_rx_bytes, + network_tx_bytes, + network_rx_rate: network_rx_rate || 0, + network_tx_rate: network_tx_rate || 0, + timestamp: now.toISOString(), + }; - dbFunctions.addContainerStats(parsed); - }), - ); - }), - ); + dbFunctions.addContainerStats(parsed); + }), + ); + }), + ); } export default storeContainerData; diff --git a/src/core/utils/calculations.ts b/src/core/utils/calculations.ts index 6919adb6..96a90cfa 100644 --- a/src/core/utils/calculations.ts +++ b/src/core/utils/calculations.ts @@ -1,49 +1,49 @@ import type Docker from "dockerode"; const calculateCpuPercent = (stats: Docker.ContainerStats): number => { - const cpuDelta = - stats.cpu_stats.cpu_usage.total_usage - - stats.precpu_stats.cpu_usage.total_usage; - const systemDelta = - stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + const cpuDelta = + stats.cpu_stats.cpu_usage.total_usage - + stats.precpu_stats.cpu_usage.total_usage; + const systemDelta = + stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; - if (cpuDelta <= 0) { - return 0.0000001; - } + if (cpuDelta <= 0) { + return 0.0000001; + } - if (systemDelta <= 0) { - return 0.0000001; - } + if (systemDelta <= 0) { + return 0.0000001; + } - const data = (cpuDelta / systemDelta) * 100; + const data = (cpuDelta / systemDelta) * 100; - if (data === null) { - return 0.0000001; - } + if (data === null) { + return 0.0000001; + } - return data * 10; + return data * 10; }; const calculateMemoryUsage = (stats: Docker.ContainerStats): number => { - if (stats.memory_stats.usage === null) { - return 0.0000001; - } + if (stats.memory_stats.usage === null) { + return 0.0000001; + } - const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; + const data = (stats.memory_stats.usage / stats.memory_stats.limit) * 100; - return data; + return data; }; function sumNetworkBytes(stats: Docker.ContainerStats) { - const nets = stats.networks ?? {}; - return Object.values(nets).reduce( - (acc, iface) => { - acc.rx += iface.rx_bytes; - acc.tx += iface.tx_bytes; - return acc; - }, - { rx: 0, tx: 0 }, - ); + const nets = stats.networks ?? {}; + return Object.values(nets).reduce( + (acc, iface) => { + acc.rx += iface.rx_bytes; + acc.tx += iface.tx_bytes; + return acc; + }, + { rx: 0, tx: 0 }, + ); } /** @@ -51,15 +51,15 @@ function sumNetworkBytes(stats: Docker.ContainerStats) { * compute bytes/sec. If no previous sample, return 0. */ function calcRate( - prevBytes: number, - prevTime: string, - currBytes: number, - currTime: Date, + prevBytes: number, + prevTime: string, + currBytes: number, + currTime: Date, ): number { - const dateConst = new Date(prevTime); - const deltaSec = (currTime.getTime() - dateConst.getTime()) / 1000; - if (deltaSec <= 0) return 0; - return (currBytes - prevBytes) / deltaSec; + const dateConst = new Date(prevTime); + const deltaSec = (currTime.getTime() - dateConst.getTime()) / 1000; + if (deltaSec <= 0) return 0; + return (currBytes - prevBytes) / deltaSec; } export { calculateCpuPercent, calculateMemoryUsage, sumNetworkBytes, calcRate }; diff --git a/src/handlers/database.ts b/src/handlers/database.ts index 76f4b4c3..901cca7b 100644 --- a/src/handlers/database.ts +++ b/src/handlers/database.ts @@ -3,13 +3,13 @@ import type { container_stats } from "~/typings/database"; import type { HostStats } from "~/typings/docker"; class databaseHandler { - getContainers(): container_stats[] { - return dbFunctions.getContainerStats(); - } + getContainers(): container_stats[] { + return dbFunctions.getContainerStats(); + } - getHosts(): HostStats[] { - return dbFunctions.getHostStats(); - } + getHosts(): HostStats[] { + return dbFunctions.getHostStats(); + } } export const DatabaseHandler = new databaseHandler(); diff --git a/src/handlers/themes.ts b/src/handlers/themes.ts index db4a5131..0a312658 100644 --- a/src/handlers/themes.ts +++ b/src/handlers/themes.ts @@ -3,46 +3,46 @@ import { logger } from "~/core/utils/logger"; import type { Theme, ThemeOptions } from "~/typings/database"; class themeHandler { - getThemes(): Theme[] { - return dbFunctions.getThemes(); - } - addTheme(theme: Theme) { - try { - const rawVars = - typeof theme.vars === "string" ? JSON.parse(theme.vars) : theme.vars; + getThemes(): Theme[] { + return dbFunctions.getThemes(); + } + addTheme(theme: Theme) { + try { + const rawVars = + typeof theme.vars === "string" ? JSON.parse(theme.vars) : theme.vars; - const cssVars = Object.entries(rawVars) - .map(([key, value]) => `--${key}: ${value};`) - .join(" "); + const cssVars = Object.entries(rawVars) + .map(([key, value]) => `--${key}: ${value};`) + .join(" "); - const varsString = `.root, #root, #docs-root { ${cssVars} }`; + const varsString = `.root, #root, #docs-root { ${cssVars} }`; - return dbFunctions.addTheme({ - ...theme, - vars: varsString, - }); - } catch (error) { - throw new Error( - `Could not save theme ${JSON.stringify(theme)}, error: ${error}`, - ); - } - } - deleteTheme(name: string) { - try { - dbFunctions.deleteTheme(name); - return "Deleted theme"; - } catch (error) { - throw new Error(`Could not save theme ${name}, error: ${error}`); - } - } - getTheme(name: string): Theme { - return dbFunctions.getSpecificTheme(name); - } - getThemeOptions(name: string): ThemeOptions { - const data = dbFunctions.getThemeOptions(name); - logger.debug(`Received ${JSON.stringify(data)}`); - return data; - } + return dbFunctions.addTheme({ + ...theme, + vars: varsString, + }); + } catch (error) { + throw new Error( + `Could not save theme ${JSON.stringify(theme)}, error: ${error}`, + ); + } + } + deleteTheme(name: string) { + try { + dbFunctions.deleteTheme(name); + return "Deleted theme"; + } catch (error) { + throw new Error(`Could not save theme ${name}, error: ${error}`); + } + } + getTheme(name: string): Theme { + return dbFunctions.getSpecificTheme(name); + } + getThemeOptions(name: string): ThemeOptions { + const data = dbFunctions.getThemeOptions(name); + logger.debug(`Received ${JSON.stringify(data)}`); + return data; + } } export const ThemeHandler = new themeHandler(); From 4e7900480e9867a69ecf1f0f8f23aa738fc7de29 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Thu, 24 Jul 2025 22:36:31 +0000 Subject: [PATCH 369/369] Update dependency graphs --- dependency-graph.mmd | 3 + dependency-graph.svg | 784 ++++++++++++++++++++++--------------------- 2 files changed, 404 insertions(+), 383 deletions(-) diff --git a/dependency-graph.mmd b/dependency-graph.mmd index 60821b11..1ea77bf5 100644 --- a/dependency-graph.mmd +++ b/dependency-graph.mmd @@ -195,6 +195,8 @@ Z-->M 1C-->J 1D-->1E 1F-->D +1F-->7 +1F-->8 1G-->D 1G-->14 1G-->O @@ -242,6 +244,7 @@ Z-->M 1T-->O 1U-->Z 1V-->D +1V-->O 1V-->7 1W-->O diff --git a/dependency-graph.svg b/dependency-graph.svg index 495b4a21..ae0b9020 100644 --- a/dependency-graph.svg +++ b/dependency-graph.svg @@ -4,11 +4,11 @@ - - + + dependency-cruiser output - + cluster_fs @@ -16,8 +16,8 @@ cluster_src - -src + +src cluster_src/core @@ -31,8 +31,8 @@ cluster_src/core/docker - -docker + +docker cluster_src/core/plugins @@ -41,13 +41,13 @@ cluster_src/core/stacks - -stacks + +stacks cluster_src/core/stacks/operations - -operations + +operations cluster_src/core/utils @@ -114,8 +114,8 @@ os - -os + +os @@ -132,8 +132,8 @@ path - -path + +path @@ -158,8 +158,8 @@ src/core/database/backup.ts->fs - - + + @@ -179,8 +179,8 @@ src/core/database/backup.ts->src/core/database/database.ts - - + + @@ -203,33 +203,33 @@ src/core/utils/logger.ts - -logger.ts + +logger.ts src/core/database/backup.ts->src/core/utils/logger.ts - - - - + + + + ~/typings/misc - -misc + +misc src/core/database/backup.ts->~/typings/misc - - + + @@ -240,8 +240,8 @@ src/core/database/database.ts->fs - - + + @@ -252,14 +252,14 @@ src/core/database/database.ts->os - - + + src/core/database/database.ts->path - - + + @@ -270,37 +270,37 @@ src/core/database/helper.ts->src/core/utils/logger.ts - - - - + + + + src/core/utils/logger.ts->path - - + + src/core/utils/logger.ts->src/core/database/_dbState.ts - - + + ~/typings/database - -database + +database src/core/utils/logger.ts->~/typings/database - - + + @@ -314,10 +314,10 @@ src/core/utils/logger.ts->src/core/database/index.ts - - - - + + + + @@ -331,10 +331,10 @@ src/core/utils/logger.ts->src/handlers/modules/logs-socket.ts - - - - + + + + @@ -354,39 +354,39 @@ src/core/database/config.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts - -containerStats.ts + +containerStats.ts src/core/database/containerStats.ts->src/core/database/database.ts - - + + src/core/database/containerStats.ts->src/core/database/helper.ts - - - - + + + + src/core/database/containerStats.ts->~/typings/database - - + + @@ -423,8 +423,8 @@ src/core/database/dockerHosts.ts->~/typings/docker - - + + @@ -452,38 +452,38 @@ src/core/database/hostStats.ts->~/typings/docker - - + + src/core/database/index.ts->src/core/database/backup.ts - - - - + + + + src/core/database/index.ts->src/core/database/database.ts - - + + src/core/database/index.ts->src/core/database/config.ts - - - - + + + + src/core/database/index.ts->src/core/database/containerStats.ts - - - - + + + + @@ -505,18 +505,18 @@ src/core/database/logs.ts - -logs.ts + +logs.ts src/core/database/index.ts->src/core/database/logs.ts - - - - + + + + @@ -564,36 +564,36 @@ src/core/database/index.ts->src/core/database/themes.ts - - - - + + + + src/core/database/logs.ts->src/core/database/database.ts - - + + src/core/database/logs.ts->src/core/database/helper.ts - - - - + + + + src/core/database/logs.ts->~/typings/database - - + + src/core/database/stacks.ts->src/core/database/database.ts - - + + @@ -606,8 +606,8 @@ src/core/database/stacks.ts->~/typings/database - - + + @@ -621,214 +621,214 @@ src/core/database/stacks.ts->src/core/utils/helpers.ts - - - - + + + + src/core/database/stores.ts->src/core/database/database.ts - - + + src/core/database/stores.ts->src/core/database/helper.ts - - - - + + + + src/core/database/themes.ts->src/core/database/database.ts - - + + src/core/database/themes.ts->src/core/database/helper.ts - - - - + + + + src/core/database/themes.ts->src/core/utils/logger.ts - - - - + + + + src/core/database/themes.ts->~/typings/database - - + + src/core/utils/helpers.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts - -client.ts + +client.ts src/core/docker/client.ts->src/core/utils/logger.ts - - + + src/core/docker/client.ts->~/typings/docker - - + + src/core/docker/scheduler.ts - -scheduler.ts + +scheduler.ts src/core/docker/scheduler.ts->src/core/utils/logger.ts - - + + src/core/docker/scheduler.ts->~/typings/database - - + + src/core/docker/scheduler.ts->src/core/database/index.ts - - + + src/core/docker/store-container-stats.ts - -store-container-stats.ts + +store-container-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-container-stats.ts - - + + src/core/docker/store-host-stats.ts - -store-host-stats.ts + +store-host-stats.ts src/core/docker/scheduler.ts->src/core/docker/store-host-stats.ts - - + + src/core/docker/store-container-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-container-stats.ts->~/typings/database - - + + src/core/docker/store-container-stats.ts->src/core/database/index.ts - - + + src/core/docker/store-container-stats.ts->src/core/docker/client.ts - - + + src/core/utils/calculations.ts - -calculations.ts + +calculations.ts src/core/docker/store-container-stats.ts->src/core/utils/calculations.ts - - + + src/core/docker/store-host-stats.ts->src/core/utils/logger.ts - - + + src/core/docker/store-host-stats.ts->~/typings/docker - - + + src/core/docker/store-host-stats.ts->src/core/database/index.ts - + src/core/docker/store-host-stats.ts->src/core/docker/client.ts - - + + ~/typings/dockerode - -dockerode + +dockerode src/core/docker/store-host-stats.ts->~/typings/dockerode - - + + @@ -842,20 +842,20 @@ src/core/plugins/loader.ts->fs - - + + src/core/plugins/loader.ts->path - - + + src/core/plugins/loader.ts->src/core/utils/logger.ts - - + + @@ -892,32 +892,32 @@ src/core/utils/change-me-checker.ts->fs/promises - - + + src/core/utils/change-me-checker.ts->src/core/utils/logger.ts - - + + src/core/plugins/plugin-manager.ts->events - - + + src/core/plugins/plugin-manager.ts->src/core/utils/logger.ts - - + + src/core/plugins/plugin-manager.ts->~/typings/docker - - + + @@ -931,76 +931,76 @@ ~/typings/plugin - -plugin + +plugin src/core/plugins/plugin-manager.ts->~/typings/plugin - - + + src/core/stacks/checker.ts - -checker.ts + +checker.ts src/core/stacks/checker.ts->src/core/utils/logger.ts - - + + src/core/stacks/checker.ts->src/core/database/index.ts - + src/core/stacks/controller.ts - -controller.ts + +controller.ts src/core/stacks/controller.ts->fs/promises - - + + src/core/stacks/controller.ts->src/core/utils/logger.ts - - + + src/core/stacks/controller.ts->~/typings/database - - + + src/core/stacks/controller.ts->src/core/database/index.ts - - + + src/core/stacks/controller.ts->src/core/stacks/checker.ts - - + + @@ -1014,53 +1014,53 @@ src/core/stacks/controller.ts->src/handlers/modules/docker-socket.ts - - + + src/core/stacks/operations/runStackCommand.ts - -runStackCommand.ts + +runStackCommand.ts src/core/stacks/controller.ts->src/core/stacks/operations/runStackCommand.ts - - + + src/core/stacks/operations/stackHelpers.ts - -stackHelpers.ts + +stackHelpers.ts src/core/stacks/controller.ts->src/core/stacks/operations/stackHelpers.ts - - + + src/core/stacks/operations/stackStatus.ts - -stackStatus.ts + +stackStatus.ts src/core/stacks/controller.ts->src/core/stacks/operations/stackStatus.ts - - + + @@ -1074,47 +1074,47 @@ src/core/stacks/controller.ts->~/typings/docker-compose - - + + - + src/handlers/modules/docker-socket.ts->src/core/utils/logger.ts - - + + - + src/handlers/modules/docker-socket.ts->~/typings/database - - + + - + src/handlers/modules/docker-socket.ts->~/typings/docker - - + + - + src/handlers/modules/docker-socket.ts->src/core/database/index.ts - - + + - + src/handlers/modules/docker-socket.ts->src/core/docker/client.ts - - + + - + src/handlers/modules/docker-socket.ts->src/core/utils/calculations.ts - - + + - + src/handlers/modules/docker-socket.ts->src/handlers/modules/logs-socket.ts @@ -1123,96 +1123,96 @@ ~/typings/websocket - -websocket + +websocket - + src/handlers/modules/docker-socket.ts->~/typings/websocket - - + + src/core/stacks/operations/runStackCommand.ts->src/core/utils/logger.ts - - + + src/core/stacks/operations/runStackCommand.ts->src/handlers/modules/docker-socket.ts - - + + src/core/stacks/operations/runStackCommand.ts->src/core/stacks/operations/stackHelpers.ts - - + + src/core/stacks/operations/runStackCommand.ts->~/typings/docker-compose - + src/core/stacks/operations/stackHelpers.ts->src/core/utils/logger.ts - - + + src/core/stacks/operations/stackHelpers.ts->src/core/database/index.ts - - + + src/core/stacks/operations/stackHelpers.ts->src/core/utils/helpers.ts - - + + src/core/stacks/operations/stackHelpers.ts->~/typings/docker-compose - - + + src/core/stacks/operations/stackStatus.ts->src/core/utils/logger.ts - - + + src/core/stacks/operations/stackStatus.ts->src/core/database/index.ts - + src/core/stacks/operations/stackStatus.ts->src/core/stacks/operations/runStackCommand.ts - - + + - + src/handlers/modules/logs-socket.ts->src/core/utils/logger.ts - - - - + + + + - + src/handlers/modules/logs-socket.ts->~/typings/database - - + + @@ -1224,25 +1224,25 @@ - + src/handlers/modules/logs-socket.ts->stream - - + + src/core/utils/package-json.ts - -package-json.ts + +package-json.ts src/core/utils/package-json.ts->package.json - - + + @@ -1262,26 +1262,26 @@ src/handlers/config.ts->src/core/database/backup.ts - - + + src/handlers/config.ts->src/core/utils/logger.ts - - + + src/handlers/config.ts->~/typings/database - - + + src/handlers/config.ts->~/typings/docker - - + + @@ -1292,80 +1292,92 @@ src/handlers/config.ts->src/core/docker/scheduler.ts - - + + src/handlers/config.ts->src/core/plugins/plugin-manager.ts - - + + src/handlers/config.ts->~/typings/plugin - - + + src/handlers/config.ts->src/core/utils/package-json.ts - - + + src/handlers/database.ts - -database.ts + +database.ts + + +src/handlers/database.ts->~/typings/database + + + + + +src/handlers/database.ts->~/typings/docker + + + src/handlers/database.ts->src/core/database/index.ts - - + + src/handlers/docker.ts - -docker.ts + +docker.ts - + src/handlers/docker.ts->src/core/utils/logger.ts - - + + - + src/handlers/docker.ts->~/typings/docker - - + + - + src/handlers/docker.ts->src/core/database/index.ts - - + + - + src/handlers/docker.ts->src/core/docker/client.ts - - + + - + src/handlers/docker.ts->~/typings/dockerode - - + + @@ -1377,22 +1389,22 @@ - + src/handlers/index.ts->src/handlers/config.ts - - + + - + src/handlers/index.ts->src/handlers/database.ts - - + + - + src/handlers/index.ts->src/handlers/docker.ts - - + + @@ -1404,7 +1416,7 @@ - + src/handlers/index.ts->src/handlers/logs.ts @@ -1419,10 +1431,10 @@ - + src/handlers/index.ts->src/handlers/modules/starter.ts - - + + @@ -1434,7 +1446,7 @@ - + src/handlers/index.ts->src/handlers/stacks.ts @@ -1449,7 +1461,7 @@ - + src/handlers/index.ts->src/handlers/store.ts @@ -1458,16 +1470,16 @@ src/handlers/themes.ts - -themes.ts + +themes.ts - + src/handlers/index.ts->src/handlers/themes.ts - - + + @@ -1479,88 +1491,94 @@ - + src/handlers/index.ts->src/handlers/utils.ts - + src/handlers/logs.ts->src/core/utils/logger.ts - - + + - + src/handlers/logs.ts->src/core/database/index.ts - + src/handlers/modules/starter.ts->src/core/docker/scheduler.ts - - + + - + src/handlers/modules/starter.ts->src/core/plugins/plugin-manager.ts - - + + - + src/handlers/modules/starter.ts->src/handlers/modules/docker-socket.ts - - + + - + src/handlers/stacks.ts->src/core/utils/logger.ts - - + + - + src/handlers/stacks.ts->~/typings/database - - + + - + src/handlers/stacks.ts->src/core/database/index.ts - + src/handlers/stacks.ts->src/core/stacks/controller.ts - - + + - + src/handlers/store.ts->src/core/database/stores.ts + + +src/handlers/themes.ts->src/core/utils/logger.ts + + + - + src/handlers/themes.ts->~/typings/database - - + + - + src/handlers/themes.ts->src/core/database/index.ts - - + + - + src/handlers/utils.ts->src/core/utils/logger.ts - - + + @@ -1572,7 +1590,7 @@ - + src/index.ts->src/handlers/index.ts