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

+74
View File
@@ -0,0 +1,74 @@
import { readdir, readFile, access } from "node:fs/promises";
import path from "node:path";
import { composeStatus, type StackRunState } from "./docker";
import { RESERVED_SLUGS } from "./slug";
export { isValidSlug, suggestSlug } from "./slug";
export interface BusinessStackSummary {
slug: string;
domain: string | null;
containerName: string;
status: StackRunState;
}
export function stacksRoot(): string {
const dir = process.env.STACKS_DIR;
if (!dir) throw new Error("STACKS_DIR must be set.");
return dir;
}
export function stackDir(slug: string): string {
return path.join(stacksRoot(), slug);
}
/** Only ever reads the DOMAIN line — deliberately never returns the rest of a business's .env (database password, AUTH_SECRET, etc.) to the UI. */
async function readDomain(envPath: string): Promise<string | null> {
try {
const content = await readFile(envPath, "utf8");
for (const line of content.split("\n")) {
const match = /^DOMAIN\s*=\s*"?([^"\n]*)"?\s*$/.exec(line.trim());
if (match?.[1]) return match[1];
}
return null;
} catch {
return null;
}
}
export async function listBusinessStacks(): Promise<BusinessStackSummary[]> {
const root = stacksRoot();
let entries: string[];
try {
entries = await readdir(root);
} catch {
return [];
}
const stacks: BusinessStackSummary[] = [];
for (const slug of entries) {
if (RESERVED_SLUGS.has(slug)) continue;
const dir = path.join(root, slug);
const composePath = path.join(dir, "compose.yaml");
try {
await access(composePath);
} catch {
continue;
}
const domain = await readDomain(path.join(dir, ".env"));
const status = await composeStatus(dir);
stacks.push({ slug, domain, containerName: `next-chair-${slug}`, status });
}
return stacks.sort((a, b) => a.slug.localeCompare(b.slug));
}
export async function stackExists(slug: string): Promise<boolean> {
try {
await access(path.join(stackDir(slug), "compose.yaml"));
return true;
} catch {
return false;
}
}