Phase ${esc(phase.id)} ${esc(phase.name)}
${esc(stateLabel(phase.state))}${doneCount} / ${phase.items.length} complete
-
${renderItems(phase.items)}
#!/usr/bin/env node // PYRE status dashboard generator. // Dependency-free: reads ../infra/status/status.json (relative to this file) // and renders ../infra/status/index.html. // // Usage (from repo root): node scripts/gen-status.mjs import { readFileSync, writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; const __dirname = dirname(fileURLToPath(import.meta.url)); const dataPath = resolve(__dirname, "../infra/status/status.json"); const outPath = resolve(__dirname, "../infra/status/index.html"); /** Escape a string for safe insertion into HTML text/attribute context. */ function esc(value) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } const data = JSON.parse(readFileSync(dataPath, "utf8")); // --- compute overall % complete from item done-counts --- let totalItems = 0; let doneItems = 0; for (const phase of data.phases) { for (const item of phase.items) { totalItems += 1; if (item.done) doneItems += 1; } } const overallPct = totalItems === 0 ? 0 : Math.round((doneItems / totalItems) * 100); // --- state badge helpers --- const STATE_LABEL = { done: "DONE", in_progress: "IN PROGRESS", todo: "TODO", }; function stateLabel(state) { return STATE_LABEL[state] || String(state).toUpperCase(); } function renderItems(items) { return items .map((item) => { const cls = item.done ? "item done" : "item"; const mark = item.done ? "✓" : "○"; // ✓ / ○ return `
${doneCount} / ${phase.items.length} complete
${esc(data.tagline)}
${doneItems} of ${totalItems} phase deliverables complete
${infraDone} / ${data.infra.length} provisioned