Management UI
CI / ci (push) Canceled after 0s

Attempt 2 of a management ui.
This commit is contained in:
Stefan Lazic committed 2026-09-05 13:39:55 +01:00
1 parent 9fe5b4f038
commit a0d39026c0
54 files changed
+8804 -267

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
/**
* Every call here runs `docker compose` with `cwd` set to a stack's own
* directory (stacks/<slug>/) rather than passing explicit -f/--env-file
* flags — compose already auto-loads `compose.yaml` and `.env` from the
* current directory, and this is the same convention Dockge itself uses,
* so a stack behaves identically whether it's started by this app, by
* Dockge, or by hand from the command line.
*
* This app's own container needs the Docker socket mounted and the
* `docker compose` CLI installed (see manager/Dockerfile) — these calls
* are executed by the HOST's dockerd via that socket, not by a nested
* Docker daemon inside this container.
*/
export interface DockerCommandResult {
stdout: string;
stderr: string;
}
async function runDockerCompose(stackDir: string, args: string[]): Promise<DockerCommandResult> {
const result = await execFileAsync("docker", ["compose", ...args], {
cwd: stackDir,
maxBuffer: 10 * 1024 * 1024,
});
return result;
}
export async function composeUp(stackDir: string, options: { forceRecreate?: boolean } = {}): Promise<void> {
const args = ["up", "-d"];
if (options.forceRecreate) args.push("--force-recreate");
await runDockerCompose(stackDir, args);
}
export async function composeDown(stackDir: string): Promise<void> {
await runDockerCompose(stackDir, ["down"]);
}
export async function composeRestart(stackDir: string): Promise<void> {
await runDockerCompose(stackDir, ["restart"]);
}
export async function composeLogs(stackDir: string, tailLines = 200): Promise<string> {
const result = await runDockerCompose(stackDir, ["logs", "--no-color", "--tail", String(tailLines)]);
// Compose writes container logs to stderr, not stdout — this is normal
// (docker's own `logs` command does the same), not a failure signal.
return result.stdout + result.stderr;
}
export type StackRunState = "running" | "stopped" | "unknown";
/** Best-effort — a stack that's never been started at all reports "stopped", not an error. */
export async function composeStatus(stackDir: string): Promise<StackRunState> {
try {
const result = await runDockerCompose(stackDir, ["ps", "--status", "running", "--format", "json"]);
return result.stdout.trim().length > 0 ? "running" : "stopped";
} catch {
return "unknown";
}
}
export async function composeExec(stackDir: string, serviceName: string, command: string[]): Promise<void> {
await runDockerCompose(stackDir, ["exec", "-T", serviceName, ...command]);
}
/** Applies pending Prisma migrations inside a freshly-started business container — retries briefly since the container's Node process may take a moment to come up after `composeUp`. */
export async function runMigrations(stackDir: string, serviceName: string): Promise<void> {
const maxAttempts = 6;
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try {
await composeExec(stackDir, serviceName, ["npx", "prisma", "migrate", "deploy"]);
return;
} catch (error) {
lastError = error;
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}
throw lastError instanceof Error ? lastError : new Error("Migrations failed after retries.");
}
/** Caddy doesn't watch its sites/ directory — adding or removing a business's .caddy file needs this to take effect. */
export async function reloadCaddy(): Promise<void> {
const containerName = process.env.CADDY_CONTAINER_NAME ?? "next-chair-caddy";
await execFileAsync("docker", ["exec", containerName, "caddy", "reload", "--config", "/etc/caddy/Caddyfile"]);
}