// `ctl-dev-kit create-dashboard --name ` (task 122 K): a fleet // dashboard, a static bundle norsk-mgr hosts on a host of its own, a shared // one or a pattern's (RFC 0006 §4.4). The starter is a live list of the // runtimes the signed-in user may see, the smallest real consumer of the // directory. import { mkdirSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; const LABEL = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/; export interface CreateDashboardOptions { dir: string; /** The dashboard's name: its registration's, and its host's when it has * one of its own. */ name: string; } function files(name: string): Record { return { "package.json": `${JSON.stringify( { name, private: true, type: "module", scripts: { dev: "vite", build: "vite build && ctl-dev-kit dashboard-manifest src dist && tar -C dist -cf dashboard.tar .", typecheck: "tsc --noEmit", }, dependencies: { "@norskvideo/ctl-sdk": "^0.1.1" }, devDependencies: { "@norskvideo/ctl-dev-kit": "^0.2.0", typescript: "^5.9.0", vite: "^8.0.0" }, }, null, 2, )}\n`, "tsconfig.json": `${JSON.stringify( { compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "Bundler", lib: ["ES2022", "DOM"], strict: true, noEmit: true, skipLibCheck: true, }, include: ["src", "vite.config.ts"], }, null, 2, )}\n`, ".gitignore": "node_modules/\ndist/\ndashboard.tar\n", "index.html": ` ${name}

Runtimes

Connecting…

RuntimeProduction or productStateNode
`, "src/main.ts": `// The runtimes the signed-in user may see, live. The platform decides who // sees what; this page only shows it. // // Name the kinds a fleet call reads with literals: \`bun run build\` derives // dashboard.json, the registration's list of what this dashboard uses, from // them, and norsk-mgr serves it no wider than that list. import { type FleetItem, fleet } from "@norskvideo/ctl-sdk/fleet"; const rows = document.getElementById("runtimes") as HTMLTableSectionElement; const status = document.getElementById("status") as HTMLParagraphElement; function cell(text: string): HTMLTableCellElement { const td = document.createElement("td"); td.textContent = text; return td; } function render(items: FleetItem[]): void { rows.replaceChildren( ...items.map((item) => { const tr = document.createElement("tr"); const { runtime } = item; tr.append( cell(String(runtime.name ?? runtime.id)), cell(String(runtime.production ?? runtime.product ?? "")), cell(item.presence?.labels["norsk.state"] ?? "unknown"), cell(String(runtime.node ?? "")), ); return tr; }), ); status.textContent = items.length === 0 ? "No runtimes you may see." : \`\${items.length} runtime(s)\`; } fleet.watch( { kind: "norsk.runtime" }, { onChange: render, onError: () => { status.textContent = "Reconnecting…"; }, }, ); `, "vite.config.ts": `import { defineConfig } from "vite"; // Relative URLs throughout: a shared host serves this dashboard under // //, its own host at the root. // // \`bun run dev\` reads a directory elsewhere at the page's own /_fleet, as a // dashboard host serves it: NORSK_DIRECTORY_URL is norsk-mgr's or norsk-ctl's // base URL, and NORSK_TOKEN an API token for it. const target = process.env.NORSK_DIRECTORY_URL; const token = process.env.NORSK_TOKEN; export default defineConfig({ base: "./", server: target ? { proxy: { "/_fleet": { target, changeOrigin: true, secure: false, rewrite: (path) => path.replace(/^\\/_fleet/, "/api/v1/fleet"), ...(token ? { headers: { authorization: \`Bearer \${token}\` } } : {}), }, }, } : {}, }); `, "README.md": `# ${name} A fleet dashboard: a static bundle norsk-mgr serves on a host of its own, reading the directory (the runtimes the signed-in user may see, with what discovery holds under each) through \`@norskvideo/ctl-sdk/fleet\`. ## Develop \`\`\`sh bun install NORSK_DIRECTORY_URL=https://mgr.example.com NORSK_TOKEN= bun run dev \`\`\` The dev server reads that norsk-mgr's (or norsk-ctl's) directory as the token's holder sees it, at the page's own \`/_fleet\`, as the dashboard host will. ## Build and register \`\`\`sh bun run build # dist/, dist/dashboard.json and dashboard.tar norsk-mgr dashboard register ${name} # a host of its own: ${name}. norsk-mgr dashboard upload ${name} dashboard.tar \`\`\` \`--hosting shared:\` serves it under \`/${name}/\` on its group's host instead; \`--hosting pattern:-{id} --scope production\` on a host per production, each answering only for its own. Uploading again replaces the bundle in place. ## What it may read \`dashboard.json\` lists the kinds the dashboard's fleet calls name, and the build derives it: name kinds as literals (\`fleet.watch({ kind: "norsk.runtime" })\`), or the build fails. The dashboard host answers no wider than that list. `, }; } export function createDashboard(opts: CreateDashboardOptions): { files: string[] } { if (!LABEL.test(opts.name) || opts.name.length > 63) { throw new Error( `the name \${JSON.stringify(opts.name)} must be one DNS label: lowercase letters, digits and '-', at most 63`, ); } if (opts.name.startsWith("t-") || opts.name.startsWith("p-")) { throw new Error(`the name \${JSON.stringify(opts.name)} starts with t- or p-, which product UI hosts use`); } const out = files(opts.name); for (const [path, content] of Object.entries(out)) { mkdirSync(dirname(join(opts.dir, path)), { recursive: true }); writeFileSync(join(opts.dir, path), content); } return { files: Object.keys(out) }; }