Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f19e88a
feat(docknode): Implement core DockNode features and Docker stack man…
Its4Nik Jan 29, 2026
e2a9242
build(dockstacks): Add @dockstat/logger dependency
Its4Nik Jan 29, 2026
3717fd1
feat(docker-client): Export core DockerClient directly and refine con…
Its4Nik Jan 29, 2026
70f9aff
feat(docknode): Integrate shared docker client and update dependencies
Its4Nik Jan 29, 2026
828107f
refactor(sqlite-wrapper): Restructure source code and modularize DB c…
Its4Nik Jan 30, 2026
6018ccd
feat(repo-cli): overhaul README documentation and rename package
Its4Nik Jan 30, 2026
1d8eb0d
feat(query-builder): Add detailed info-level logging to query executi…
Its4Nik Jan 31, 2026
be2ce0e
formatting
Its4Nik Jan 31, 2026
264da1d
Feat theme handler (#73)
Its4Nik Feb 5, 2026
ec9c475
feat(core, ui, api, docknode): Implement theme management, enhance lo…
Its4Nik Feb 5, 2026
fd3f0af
feat(hotkeys, node, ui): Introduce configurable hotkeys and DockNode …
Its4Nik Feb 5, 2026
da7bf37
feat(hotkeys): Introduce `useHotkey` hook and add sidebar toggle
Its4Nik Feb 5, 2026
add16e2
fix(formatting): adjusted imports
Its4Nik Feb 5, 2026
e3378fc
feat(docknode-management): Introduce API, UI, and backend for DockNod…
Its4Nik Feb 8, 2026
235a282
chore(ci): Lint [skip ci]
Its4Nik Feb 8, 2026
3a09683
refactor(ui): Enhance Slides animation and DockNodeCard UI
Its4Nik Feb 8, 2026
29bcce3
Merge branch 'feat-DockStacks-and-DockNode' of github.com:Its4Nik/Doc…
Its4Nik Feb 8, 2026
f27d45c
Feat settings page (#76)
Its4Nik Feb 24, 2026
ffe927c
82 dynamic theme editor (#84)
Its4Nik Mar 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
"@dockstat/db": "workspace:*",
"@dockstat/docker-client": "workspace:*",
"@dockstat/logger": "workspace:*",
"@dockstat/theme-handler": "workspace:*",
"@dockstat/docknode": "workspace:*",
"@dockstat/plugin-handler": "workspace:*",
"@dockstat/sqlite-wrapper": "workspace:*",
"@dockstat/typings": "workspace:*",
"@dockstat/repo-cli": "workspace:*",
"@dockstat/ui": "workspace:*",
"@dockstat/utils": "workspace:*",
"@elysiajs/eden": "catalog:",
"@elysiajs/cors": "^1.4.0",
"@elysiajs/openapi": "^1.4.11",
"@elysiajs/server-timing": "^1.4.0",
Expand Down
108 changes: 108 additions & 0 deletions apps/api/src/docknode/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import type { DockNodeTreaty } from "@dockstat/docknode/treaty"
import type Logger from "@dockstat/logger"
import { column, type DB, type QueryBuilder } from "@dockstat/sqlite-wrapper"
import type { DockStatConfigTableType } from "@dockstat/typings/types"
import { treaty } from "@elysiajs/eden"
import type { DockNodeTable } from "./type"

class DockNodeHandler {
private table: QueryBuilder<DockNodeTable>
private logger: Logger
private loadedNodes = new Map<number, ReturnType<typeof treaty<DockNodeTreaty>>["api"]>()

constructor(DB: DB, logger: Logger) {
this.logger = logger.spawn("DNH")
this.table = DB.createTable<DockNodeTable>(
"docknode-register",
{
id: column.id(),
name: column.text(),
host: column.text(),
port: column.integer(),
useSSL: column.boolean(),
keys: column.foreignKey<DockStatConfigTableType>("config", "keys", {
references: {
onDelete: "NO ACTION",
onUpdate: "CASCADE",
column: "keys",
table: "config",
},
type: "JSON",
}),
timeout: column.integer({ default: 60 }),
},
{ ifNotExists: true }
)

this.logger.info("DockNode-Hanlder initialising")

const allDockNodeClients = this.table.select(["*"]).all()

for (const node of allDockNodeClients) {
this.loadedNodes.set(
Number(node.id),
treaty<DockNodeTreaty>(`${node.useSSL ? "https://" : "http://"}${node.host}:${node.port}`)
.api
)
}

this.logger.info("DockNode-Hanlder initialized.")
}

private setLoadedNode(node: DockNodeTable) {
return this.loadedNodes.set(
Number(node.id),
treaty<DockNodeTreaty>(`${node.useSSL ? "https://" : "http://"}${node.host}:${node.port}`).api
)
}

async getAllNodes() {
this.logger.info("Getting all nodes")
const allNodes = this.table.select(["*"]).all()

this.logger.debug(`Got ${allNodes.length} node(s)`)

const res = await Promise.all(
allNodes.map(async (n) => {
this.logger.debug(`Getting online state for ${n.id}`)
const dnc = this.loadedNodes.get(Number(n.id))

if (!dnc) {
return {
...n,
isReachable: "DockNode not initialised",
}
}

const state = (await dnc?.status.get())?.data ?? "NO"

this.logger.info(`State for ${n.id}: ${state}`)

return {
...n,
isReachable: state,
}
})
)

return res
}

createNode(cfg: Omit<DockNodeTable, "keys">) {
const insertRes = this.table.insert(cfg)

const node = this.table.select(["*"]).where({ id: insertRes.insertId }).first()

if (node !== null) {
this.setLoadedNode(node)
}

return insertRes
}

delteNode(id: number) {
return this.table.where({ id: id }).delete()
}
}

export default DockNodeHandler
11 changes: 11 additions & 0 deletions apps/api/src/docknode/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { DockStatConfigTableType } from "@dockstat/typings/types"

export type DockNodeTable = {
id?: number
name: string
host: string
port: number
useSSL: boolean
timeout: number
keys: DockStatConfigTableType["keys"]
}
4 changes: 4 additions & 0 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import MetricsMiddleware from "./middleware/metrics"
import DBRoutes from "./routes/db"
import DockerRoutes from "./routes/docker"
import DockStatMiscRoutes from "./routes/misc"
import { DockNodeElyisa } from "./routes/node"
import PluginRoutes from "./routes/plugins"
import RepositoryRoutes from "./routes/repositories"
import StatusRoutes from "./routes/status"
import ThemeRoutes from "./routes/themes"
import DockStatWebsockets from "./websockets"

const PORT = Bun.env.DOCKSTATAPI_PORT || 3030
Expand All @@ -25,7 +27,9 @@ export const DockStatAPI = new Elysia({ prefix: "/api/v2" })
.use(PluginRoutes)
.use(DockStatMiscRoutes)
.use(RepositoryRoutes)
.use(ThemeRoutes)
.use(DockStatWebsockets)
.use(DockNodeElyisa)
.listen(PORT)

const hostnameAndPort = `${DockStatAPI.server?.hostname}:${DockStatAPI.server?.port}`
Expand Down
6 changes: 4 additions & 2 deletions apps/api/src/middleware/metrics/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,10 @@ const MetricsMiddleware = (app: Elysia) => {
savePersistedMetrics()
}

logger.debug("Tracked metrics", headers["x-dockstatapi-reqid"] ?? undefined)
logger.info("Request finished", headers["x-dockstatapi-reqid"] ?? undefined)
logger.info(
`Request on ${new URL(request.url).pathname} finished`,
headers["x-dockstatapi-reqid"] ?? undefined
)
}
)
.onError(
Expand Down
20 changes: 20 additions & 0 deletions apps/api/src/models/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ export namespace DatabaseModel {
200: configRes,
400: error,
})

export const hotkeyRes = t.Object({
success: t.Literal(true),
message: t.String(),
data: t.Array(
t.Object({
action: t.String(),
key: t.String(),
})
),
})

export const hotkeyBody = t.Pick(updateBody, t.Literal("hotkeys"))

export const additionalSettingsBody = t.Pick(updateBody, t.Literal("additionalSettings"))

export const additionalSettingsRes = t.Object({
success: t.Boolean(),
message: t.String(),
})
}

export namespace RepositoryModel {
Expand Down
22 changes: 22 additions & 0 deletions apps/api/src/models/docknode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { t } from "elysia"

export namespace DockNodeModel {
export const dockNode_registryTable = t.Object({
id: t.Integer(),
name: t.String({ minLength: 5 }),
host: t.String({ minLength: 5 }),
port: t.Number(),
useSSL: t.Boolean(),
timeout: t.Number(),
keys: t.Unknown(),
})

export const createBody = t.Omit(
t.Omit(dockNode_registryTable, t.Literal("keys")),
t.Literal("id")
)

export const deleteBody = t.Object({
id: t.Number(),
})
}
84 changes: 84 additions & 0 deletions apps/api/src/routes/db.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SQLQueryBindings } from "bun:sqlite"
import type { RepoFile } from "@dockstat/repo-cli/types"
import type { DockStatConfigTableType } from "@dockstat/typings/types"
import { extractErrorMessage, repo } from "@dockstat/utils"
Expand All @@ -13,6 +14,51 @@ const DBRoutes = new Elysia({
tags: ["DB"],
},
})
.get("/details", () => {
const schema = DockStatDB._sqliteWrapper.getSchema()
const integrity = DockStatDB._sqliteWrapper.integrityCheck()
const backups = DockStatDB._sqliteWrapper.listBackups()
const path = DockStatDB._dbPath

const info: Record<
string,
{
table: {
name: string
type: string
sql: string
}
info: {
cid: number
name: string
type: string
notnull: number
dflt_value: SQLQueryBindings
pk: number
}[]
}
> = {}

for (const table of schema) {
const i = DockStatDB._sqliteWrapper.getTableInfo(table.name)
info[table.name] = { table, info: i }
}

return {
info,
integrity,
path,
backups,
}
})
.get(
"/details/:tableName/all",
({ params }) => DockStatDB._sqliteWrapper.table(params.tableName).select(["*"]).all(),
{
params: t.Object({ tableName: t.String() }),
}
)

// ==================== Config Routes ====================
.post(
"config",
Expand Down Expand Up @@ -114,6 +160,44 @@ const DBRoutes = new Elysia({
}
)

.post(
"/config/hotkey",
({ body }) => DockStatDB.configTable.where({ id: 0 }).update({ hotkeys: body.hotkeys }),
{
body: DatabaseModel.hotkeyBody,
}
)

.post(
"/config/additionalSettings",
({ body, status }) => {
try {
DockStatDB.configTable
.where({ id: 0 })
.update({ additionalSettings: body.additionalSettings })

return status(200, {
success: true,
message: "Additional settings updated successfully",
data: body.additionalSettings,
})
} catch (error) {
const errorMessage = extractErrorMessage(error, "Error while updating additional settings")
return status(400, {
success: false,
message: errorMessage,
})
}
},
{
body: DatabaseModel.additionalSettingsBody,
response: {
200: DatabaseModel.additionalSettingsRes,
400: DatabaseModel.additionalSettingsRes,
},
}
)

// ==================== Repository Routes ====================
.get(
"repositories",
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/routes/docker/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const DockerContainerElysia = new Elysia({
},
})
.get("/all-containers", async ({ status }) => {
const CC = await DCM.getContainerCount()
const CC = await DCM.getAllContainerStats()
return status(200, CC)
})
.get("/all/:clientId", async ({ params: { clientId } }) => await DCM.getAllContainers(clientId), {
Expand Down
20 changes: 20 additions & 0 deletions apps/api/src/routes/node/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Elysia from "elysia"
import { DockStatDB } from "../../database"
import DockNodeHandler from "../../docknode"
import BaseLogger from "../../logger"
import { DockNodeModel } from "../../models/docknode"

const DNH = new DockNodeHandler(DockStatDB._sqliteWrapper, BaseLogger)

export const DockNodeElyisa = new Elysia({ prefix: "/node", detail: { tags: ["DockNode"] } })
.get("/", async ({ status }) => {
return status(200, await DNH.getAllNodes())
})
.post(
"/",
({ body }) => {
DNH.createNode(body)
},
{ body: DockNodeModel.createBody }
)
.delete("/", ({ body }) => DNH.delteNode(body.id), { body: DockNodeModel.deleteBody })
12 changes: 6 additions & 6 deletions apps/api/src/routes/repositories/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { RepoManifestType, RepoType } from "@dockstat/typings/types"
import { parseFromDBToRepoLink } from "@dockstat/utils/src/repo"
import { repo } from "@dockstat/utils"
import Elysia from "elysia"
import { DockStatDB } from "../../database"

Expand All @@ -20,8 +20,8 @@ const RepositoryRoutes = new Elysia({
}
> = {}

for (const repo of allRepos) {
const link = parseFromDBToRepoLink(repo.type, repo.source)
for (const repoElement of allRepos) {
const link = repo.parseFromDBToRepoLink(repoElement.type, repoElement.source)

try {
const response = await fetch(link)
Expand Down Expand Up @@ -53,10 +53,10 @@ const RepositoryRoutes = new Elysia({
}
}

result[repo.name] = {
result[repoElement.name] = {
data: data as RepoManifestType,
type: repo.type,
repoSource: repo.source,
type: repoElement.type,
repoSource: repoElement.source,
}
} catch (error) {
console.error(`Error processing ${link}:`, error)
Expand Down
Loading