From 956f6624b7ba4d01350367e5014387b5368c3624 Mon Sep 17 00:00:00 2001 From: Its4Nik Date: Sun, 29 Jun 2025 17:36:23 +0200 Subject: [PATCH 01/30] ``` 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 02/30] 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 03/30] 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 04/30] 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 05/30] 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 06/30] 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 07/30] 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 08/30] 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 09/30] 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 10/30] 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 11/30] 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 12/30] 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 13/30] 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 14/30] 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 15/30] 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 16/30] 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 17/30] 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 18/30] 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 19/30] 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 20/30] 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 21/30] 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 22/30] 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 23/30] 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 24/30] 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 25/30] 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 26/30] 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 27/30] 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 28/30] 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 29/30] 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 30/30] 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